---
# System prepended metadata

title: Making Raspberry Pi Zero boot without SD card

---

# Making Raspberry Pi Zero boot without SD card

[TOC]

Latest version of this document will always be [here](https://github.com/ralsina/zero-nfsroot)

This is **heavily** based on [this page](https://dev.webonomic.nl/how-to-run-or-boot-raspbian-on-a-raspberry-pi-zero-without-an-sd-card) with some tweaks to make it boot 
exactly the way I want it to and configure the network like I wanted. Also, lots of things are "reverse engineered" from [ClusterHAT](hgttp://clusterhat.com) meaning I have one and saw how it works.

**Note:** These are the working notes of how **I** did it. I could have just done it 
but taking notes is good because:

> The difference between screwing around and science is writing it down 
> -- Adam Savage


## Goals

After all this is done, we should have one or more raspberry pi zeros booting

* Off NFS without SD cards
* Using network via USB-gadget-ethernet
* With network configured to be part of the local LAN via a bridge

## On the local network

Configure a range for static IP addresses. How to do that depends on your
setup, but usually implies logging into your router and changing its DHCP
server configuration. I have the network `192.168.0.0/24` and saved
IPs `201 ... 254` for static addresses.

## On the host machine

Again, details on *how* to do these things will vary depending on your 
Linux distribution or whatever operating system you are using (I don't expect
anything that is not Linux will work)

* Install a NFS server
* Install [USBBoot](https://github.com/raspberrypi/usbboot)
* Install bridge-utils
* Connect yourself to the network via ethernet (it can probably be made to work with wifi)
* Enable IP forwarding and set your IPTables FORWARD policy to accept.

### Configure an ethernet bridge

Details may vary but we want an ethernet bridge that includes your ethernet interface.
For example, mine is `enp4s0f3u1u4` and my bridge is `br0`so:

If you are using NetworkManager, it has a GUI to configure it that works just fine.

This is the bridge to which we will also connect the zeros' ethernets so they become part 
of the LAN.

```sh
$ brctl show
bridge name           bridge id              STP enabled         interfaces
br0                   8000.a2d599084fcf      yes                 enp4s0f3u1u4
```

If all went well, you will have an IP address on the `br0` interface and not in your "real" ethernet:

```sh
$ ifconfig -a

br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.0.72  netmask 255.255.255.0  broadcast 192.168.0.255
        inet6 2800:810:505:2c8::1004  prefixlen 128  scopeid 0x0<global>
        inet6 fe80::ef7a:62ed:f9:5ce5  prefixlen 64  scopeid 0x20<link>
        inet6 2800:810:505:2c8:2dff:72aa:2fce:b293  prefixlen 64  scopeid 0x0<global>
        ether a2:d5:99:08:4f:cf  txqueuelen 1000  (Ethernet)
        RX packets 82794  bytes 123703379 (117.9 MiB)
        RX errors 0  dropped 178  overruns 0  frame 0
        TX packets 77001  bytes 49355088 (47.0 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

enp4s0f3u1u4: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether 28:ee:52:1a:8c:20  txqueuelen 1000  (Ethernet)
        RX packets 133110  bytes 125755066 (119.9 MiB)
        RX errors 0  dropped 78  overruns 0  frame 0
        TX packets 66604  bytes 10713246 (10.2 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
```

## Create the NFS root

There are two ways to do this, the easy way and the hard way. You choose.

### The Hard Way

We will boot a raspberry pi zero, configure it, and then turn its filesystem into
a NFS root. Since we will know **exactly** what we changed we can then use that as
the basis for just copying and modifying it for as many machines as we want, which is the easy way. So, you *could* just skip this section and do what's described in the next one.
You would be, of course, a chicken.

Hello non-feathered readers, let's do this.

* Get raspberry pi OS 32-bit light (or whatever) into a SD card. 
  Doesn't need to be a fast or large SD card, since we will only use it for a few minutes.
  
  If you will try to do this using something other than raspberry pi os then the
  details of *how* to do it will change, but the concepts should still work.

Now we are going to make some changes in the configuration of that SD card before booting
from it.

* Create empty `boot/ssh` file so it has SSH enabled on boot.
* Edit `boot/config.txt` and add this at the bottom:

```
# enable OTG
dtoverlay=dwc2
# set initramfs
initramfs initrd.img followkernel
```

* Edit `boot/cmdline.txt` and add this at the end of the 1st line:

`modules-load=dwc2,g_ether`


* Edit `etc/network/interfaces.d/usb0` to configure its `usb0` interface:

```
auto usb0
allow-hotplug usb0
iface usb0 inet static
  address 192.168.0.201/24
  gateway 192.168.0.1
  dns-nameservers 8.8.8.8 1.1.1.1 9.9.9.9
```

* Enable ssh for the future

`sudo systemctl enable ssh`

At this point we should be able to boot off this SD card, so insert the SD card into the Zero, plug the Zero into your computer via USB, plug some monitor in its HDMI interface and let's see what happens.

The first boot is pretty slow and convoluted because it will do things like resize the filesystem to use the whole SD card and whatnot. But eventually it should finish.

At some point, your computer (not the Zero!) will have a new network interface.
In my case it's called `enp4s0f3u1u1`

**NOTE:** The actual USB port on which you plug it is important, because it determines the
name of the ethernet interface to which your Zero is connected, so ... take notes.

We want that ethernet interface to be added to the bridge, so ... *make it so* 

```sh
$ brctl show
bridge name           bridge id              STP enabled         interfaces
br0                   8000.a2d599084fcf      yes                 enp4s0f3u1u4
                                                                 enp4s0f3u1u1
```

Reboot the Zero, and now it should boot with the static IP we configured and say
something like

"My IP address is 192.168.0.201"

If everything went *particularly* well you should even be able to SSH into it!

```sh
$  ssh pi@192.168.0.201
The authenticity of host '192.168.0.201 (192.168.0.201)' can't be established.
ED25519 key fingerprint is SHA256:gfs9NKRE1y7Oy0lrA3F9dcXg56JEmN0yyFRoAo8o86M.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.0.201' (ED25519) to the list of known hosts.
pi@192.168.0.201's password: 
Linux raspberrypi 5.10.17+ #1414 Fri Apr 30 13:16:27 BST 2021 armv6l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

SSH is enabled and the default password for the 'pi' user has not been changed.
This is a security risk - please login as the 'pi' user and type 'passwd' to set a new password.

pi@raspberrypi:~$ 
```

Next step is to create a ramdisk that has enough "stuff" (technical term) in it to
boot without the SD card.

Edit `/etc/initramfs-tools/modules` so it has *at least* this:

```
g_ether
libcomposite
u_ether
udc-core
usb_f_rndis
usb_f_ecm
```

Create the ramdisk:

```
$ sudo update-initramfs -c -k `uname -r`
update-initramfs: Generating /boot/initrd.img-5.10.17+
$ sudo mv /boot/initrd.img-5.10.17+ /boot/initrd.img
```


Reboot one last time to make sure everything works.

Now we will take this SD card, and turn it into a NFS root so we can boot our Zero off
it without any onboard storage.

* Take the SD card and put it back in your conmputer.

As root, create a "p1" folder somewhere and copy the contents of the SD card in it preserving permissions and such:

```
# mkdir -p /zeros/p1
# cp -a /run/[whatever]/rootfs/* /zeros/p1/
# cp -a /run/[whatever]/boot/* /zeros/p1/boot/
```

We'll need to configure a few things to make the Zero boot with root on NFS:

* Change `/root/p1/boot/cmdline.txt` to look more or less like this (adjust as needed):

```
console=serial0,115200 console=tty1 root=/dev/nfs nfsroot=192.168.0.72:/zeros/p1 rw elevator=deadline fsck.mode=skip rootwait modules-load=dwc2,g_ether ip=192.168.0.201:192.168.0.1::255.255.255.0:p1:usb0:static rootwait
```

Explanation:

* Set root to be nfs
* Set nfsroot to `{the ip of your NFS server}:/zeros/p1` 
* Don't try to FSCK a NFS server
* Configure `usb0` network interface to be static, IP is `192.168.0.201`, etc.

* Share `/zeros/p1` via NFS with the IP of the zero by adding this in `/etc/exports`:

```
/zeros/p1 192.168.0.201(rw,async,insecure,no_subtree_check,no_root_squash)
```

Then run `exportfs -arv`

* Edit `/zeros/p1/etc/fstab` and remove references to local devices. That usually means 
  that the only uncommented line will be the one about `proc`. Mine looks like this:
  
```
proc            /proc           proc    defaults          0       0
# a swapfile is not a swap partition, no line here
#   use  dphys-swapfile swap[on|off]  for that
```

At this point, your Zero should be able to boot without a SD card if we give it a little help. Plug it in (without SD card, of course) and run this:

```
$ sudo rpiboot -v -d /zeros/p1/boot/
```

It should start telling you how it's sending files to a device. What's happening is that
`rpiboot` is giving your Zero the files it needs to *start* booting, such as the kernel, the ramdisk and so on. After a while it should be fully booted, with network and SSH active.

At this point we have a fully functional and confitured NFS root tree. Let's save it.

As root:

```
# cd /zerros/p1
# tar cfJ ~/pi1.tar.xz .
```

You will now have a fully-configured image in ~/pi1.tar.xz. I have made mine available [here](https://drive.google.com/file/d/19rwrtPAysFs_3XwJBUaAYcnXG6EdF4wi/view?usp=sharing)

### The Easy Way

If you have read "The Hard Way" just smirk condescendingly and move along.

* Download [pi1.tar.xz](https://drive.google.com/file/d/19rwrtPAysFs_3XwJBUaAYcnXG6EdF4wi/view?usp=sharing) 

As root in your machine:

```
# mkdir -p /zeros/p1
# cd /zeros/p1
# tar xvf /wheverver/you/put/it/pi1.tar.xz
```

* Choose a static IP for the Zero (I used 192.168.0.201)

* Edit the following files to make sure they agree with your network configuration:

* `/zeros/p1/boot/cmdline`
* `/zeros/p1/etc/network/interfaces.d/usb0`

* Export `/zeros/p1` via NFS for the IP of the Zero by putting something like this in `/etc/exports`

```
/zeros/p1 192.168.0.201(rw,async,insecure,no_subtree_check,no_root_squash)
```

Then run `exportfs -arv`

At this point, your Zero should be able to boot without a SD card if we give it a little help. Plug it in (without SD card, of course) and run this:

```
$ sudo rpiboot -v -d /zeros/p1/boot/
```

It should start telling you how it's sending files to a device. What's happening is that
`rpiboot` is giving your Zero the files it needs to *start* booting, such as the kernel, the ramdisk and so on. After a while it should be fully booted, but the network won't work.

At some point, your computer (not the Zero!) will have a new network interface.
In my case it's called `enp4s0f3u1u1`

**NOTE:** The actual USB port on which you plug it is important, because it determines the
name of the ethernet interface to which your Zero is connected, so ... take notes.

We want that ethernet interface to be added to the bridge, so ... *make it so* 

```sh
$ brctl show
bridge name           bridge id              STP enabled         interfaces
br0                   8000.a2d599084fcf      yes                 enp4s0f3u1u4
                                                                 enp4s0f3u1u1
```

Reboot the Zero, and now it should boot with the static IP we configured and say
something like

"My IP address is 192.168.0.201"

If everything went *particularly* well you should even be able to SSH into it!

Congratulations, you made it the easy way. WHICH IS GOOD ENOUGH.

## The second Zero

If you have more than one Zero (you should!) then you may want to automate the second one further. Actually, after we are done with this part we'll have something easier than "the easy way".

What's different for the second zero?

* IP is `192.168.0.202` instead of `192.168.0.201`
* Its nfsroot is `/zeros/p2` instead of `/zeros/p1`
* The interface we add to the bridge will be different.

Further, if this was configured in a whole different network and system that's not my own, what would change?

* Default route, DNS servers, network mask are different
* IP address of the NFS server is different
* Path to the nfsroots is different
* Ethernet interface name and bridge name will be different

That's a surprisingly small number of things to change to boot N different systems off
a server.

Nothing else?

Well, we'll need to use one other feature of rpiboot so it works correctly for more than 
one device, but it's easy.

So, we need a simple way to "template" our tarball with the nfsroot and make it accept a few parameters and configure a few files.

**Note:** We know *exactly* what files need templating *because I took notes.* TAKE NOTES WHEN YOU DO STUFF.

### Templated nfsroot

The changes we need to do to add a second, third or whatever many Zeros we want are pretty mechanical. So, let's automate them. Or not, feel free to do things by hand like a farmer.

* Template language: jinja2
* Needed tools: [j2cli](https://github.com/kolypto/j2cli)

Not to go into details but here is the file with the configuration data [config.yaml](https://github.com/ralsina/zero-nfsroot/blob/main/config.yaml) you can edit and the only files that need templating are these:

* `/zeros/pX/boot/cmdline.txt` [templated here](https://github.com/ralsina/zero-nfsroot/blob/main/cmdline.txt.j2)
* `/zeros/pX/etc/network/interfaces.d/usb0` [templated here](https://github.com/ralsina/zero-nfsroot/blob/main/usb0.j2)

So, if you expand the tarball as `/zeros/p3` you can configure it doing something like this:

```
$ j2 cmdline.txt.j2 config.yaml -o /zeros/p3/boot/cmdline.txt
$ j2 usb0.j2 config.yaml -o /zeros/p3/etc/network/interfaces/usb0 
```

Now configure `/etc/exports` and then boot the Zero and do the whole "add interfaces to the bridge" thing.

### Making rpiboot less annoying

Currently to boot, say, raspbery pi #3 we need to go to `/zeros/pi3/boot` and run rpiboot. That is ... not practical.

Luckily, rpiboot supports booting multiple devices independently **as long as we know their USB path.**

Their what? Their USB path. So, USB is shaped like a tree. Here's mine (yes, my computer is complicated):

```
$  lsusb -t
/:  Bus 04.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 10000M
/:  Bus 03.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/2p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 2: Dev 4, If 0, Class=Vendor Specific Class, Driver=, 480M
    |__ Port 2: Dev 3, If 0, Class=Wireless, Driver=btusb, 12M
    |__ Port 2: Dev 3, If 1, Class=Wireless, Driver=btusb, 12M
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 10000M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 5000M
        |__ Port 4: Dev 8, If 0, Class=Vendor Specific Class, Driver=r8152, 5000M
    |__ Port 2: Dev 7, If 0, Class=Hub, Driver=hub/4p, 5000M
    |__ Port 3: Dev 3, If 0, Class=Hub, Driver=hub/4p, 5000M
        |__ Port 4: Dev 4, If 0, Class=Hub, Driver=hub/4p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 1: Dev 110, If 0, Class=Vendor Specific Class, Driver=, 12M
    |__ Port 2: Dev 11, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 1: Dev 20, If 0, Class=Hub, Driver=hub/4p, 480M
            |__ Port 3: Dev 21, If 0, Class=Human Interface Device, Driver=usbhid, 12M
            |__ Port 3: Dev 21, If 1, Class=Human Interface Device, Driver=usbhid, 12M
        |__ Port 4: Dev 22, If 2, Class=Audio, Driver=snd-usb-audio, 12M
        |__ Port 4: Dev 22, If 0, Class=Audio, Driver=snd-usb-audio, 12M
        |__ Port 4: Dev 22, If 3, Class=Human Interface Device, Driver=usbhid, 12M
        |__ Port 4: Dev 22, If 1, Class=Audio, Driver=snd-usb-audio, 12M
        |__ Port 2: Dev 23, If 1, Class=Human Interface Device, Driver=usbhid, 12M
        |__ Port 2: Dev 23, If 2, Class=Human Interface Device, Driver=usbhid, 12M
        |__ Port 2: Dev 23, If 0, Class=Human Interface Device, Driver=usbhid, 12M
    |__ Port 3: Dev 3, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 1: Dev 45, If 0, Class=Hub, Driver=hub/4p, 480M
            |__ Port 1: Dev 92, If 2, Class=Audio, Driver=snd-usb-audio, 480M
            |__ Port 1: Dev 92, If 0, Class=Video, Driver=uvcvideo, 480M
            |__ Port 1: Dev 92, If 3, Class=Audio, Driver=snd-usb-audio, 480M
            |__ Port 1: Dev 92, If 1, Class=Video, Driver=uvcvideo, 480M
            |__ Port 1: Dev 92, If 4, Class=Human Interface Device, Driver=usbhid, 480M
            |__ Port 4: Dev 49, If 0, Class=Mass Storage, Driver=usb-storage, 480M
        |__ Port 4: Dev 52, If 0, Class=Hub, Driver=hub/4p, 480M
    |__ Port 4: Dev 4, If 1, Class=Video, Driver=uvcvideo, 480M
    |__ Port 4: Dev 4, If 0, Class=Video, Driver=uvcvideo, 480M
```

But don't worry, you don't need to understand all that, you just need to run one command and look carefully.

Unplug your Zero, and now boot it running this command:

```
rpiboot -vv -d /zeros/p1/boot/
```

Right before it starts sending files to the Zero it will show something like this:

```
Found device 1 idVendor=0x0a5c idProduct=0x2763
Bus: 1, Device: 113 Path: 1-1.1
Found candidate Compute Module...Loading: /zeros/p1/boot//bootcode.bin
Device located successfully
```

See that 1-1.1 ? That's the path (yours will be different)

Now create a `/zeros/boot` and create in it a link called `1-1.1` pointing to `/zeros/p1/boot` and repeat for every zero you want to boot like this.

```
# mkdir /zeros/boot
# cp /zeros/p1/boot/bootcode.bin /zeros/boot/
# ln -s /zeros/p1/boot/ /zeros/boot/1-1.1
```

It should look like this (with yout own USB paths instead of mine):

```
# ls -l /zeros/boot/
total 52
lrwxrwxrwx 1 root root    15 Jul 24 17:56 1-1.1 -> /zeros/p1/boot/
lrwxrwxrwx 1 root root    15 Jul 24 18:20 1-1.2 -> /zeros/p2/boot/
-rw-r--r-- 1 root root 52456 Jul 24 17:58 bootcode.bin
```

Now, if you run `rpiboot -v -o -d /zeros/boot -l` this will follow the link with the name of the USB path of the device and loop and try again and so on, booting as many Zeros as you need as many times as needed.

You can, of course, make this a systemd service or whatever.