Build Your Own Home & Travel Router with a Raspberry Pi 5

A complete guide to building a portable, self-contained router that serves as your main home router for trusted devices day-to-day, then unplugs and travels with you — providing Wi-Fi, mobile data failover, ad-blocking, encrypted networking, and a library of offline content, all from a device smaller than a paperback book.

This is the kind of practical project I talked about in Forget Climate Action — We Need Climate Resilience — building real tools that work when the usual infrastructure doesn’t.


What You’re Building

This is a two-device setup:

The Pi 5 (your router) — a small box that:

  • Creates its own Wi-Fi network for your trusted devices — at home, in a hotel, campsite, motorhome, or ferry
  • Connects to the internet via your ISP router at home, or a hotel wired connection / 4G/5G mobile data when travelling
  • Routes all your traffic through an encrypted Tailscale mesh, so nobody on the upstream network can see what you’re doing
  • Blocks ads and trackers for every device connected to it
  • Hosts offline content — Wikipedia, maps, e-books, music, films — accessible even with no internet at all
  • Shares files between your devices over your own private network

The Pi 3 (your coordination server) — a tiny, always-on box that:

  • Stays at home, plugged into your ISP router
  • Runs Headscale, which lets all your Tailscale-connected devices find each other
  • Replaces the need for a cloud server or VPS — everything stays on hardware you own
  • Uses barely any power or resources

At home: the Pi 5 plugs into your ISP router and acts as the main router for your phones, laptops, and other trusted devices. Your ISP router handles only IoT gadgets and guest Wi-Fi. The Pi 3 sits quietly on the same network running the coordination server.

On the move: you unplug the Pi 5 and take it with you. It connects to hotel wired internet or 4G/5G, and your devices connect to it exactly as they did at home. The Pi 3 back home keeps the Tailscale mesh running so your phone can still reach the travel router (and vice versa) from anywhere.

No cloud services. No third-party tunnels. No public-facing anything. The only way to reach your network is through Tailscale, which requires your Headscale server to authenticate.


What You’ll Need

The Core Parts

PartWhat It DoesApprox. Price
Raspberry Pi 5 (8GB)The brain of the router~£80
ZDE ZP596 HAT + ZC506 caseAdds a second wired Ethernet port and a slot for the Wi-Fi card, all in an aluminium case~£45
Intel AX210 Wi-Fi/Bluetooth cardProvides fast Wi-Fi 6E to create your wireless network~£15
2TB 2.5" SATA SSDStores the operating system and all your offline content~£80–100
USB-to-SATA enclosureConnects the SSD to the Pi via USB~£10–15
30W USB-C PD power bankPowers the Pi 5 at home (plugged in) and on the move (battery) — must support USB-C Power Delivery at 5V 5A. A 20,000 mAh bank gives roughly 6–8 hours of portable use~£25–40
MicroSD card (any small one)Only needed temporarily for the initial setupYou probably have one

For the home coordination server:

PartWhat It DoesApprox. Price
Raspberry Pi 3 (any model)Runs Headscale at home — an old/spare Pi is perfect for this~£0–30 (you may already have one)
MicroSD card (8GB+)Runs the Pi 3’s operating system~£5
Micro-USB power supplyPowers the Pi 3~£5–10
Ethernet cableConnects the Pi 3 to your ISP router~£3
USB hard drive (2TB+)Mirrors the Pi 5’s content (Wikipedia, maps, media) so you don’t have to re-download everything if the SSD dies or the router is lost~£50–80

Total: roughly £310–390 (less if you already have a spare Pi 3 and/or a spare drive)

Optional: Mobile Data

PartWhat It DoesApprox. Price
Quectel RM502Q-AE 5G modemAdds 5G/4G mobile data as a backup internet connection~£80–120
EXVIST 5GM2A USB adapter boardConnects the modem to the Pi via USB and provides the power it needs~£20–30
SIM card with data planProvides mobile data — any standard SIM worksVaries

Or for a simpler (4G-only) option:

PartWhat It DoesApprox. Price
Huawei E3372 4G USB stickPlug-and-play 4G modem, much simpler to set up~£30–40
SIM card with data planProvides mobile dataVaries

You’ll Also Need (One-Off Setup)

  • A separate computer to prepare the SSD and access the Pi during setup
  • An Ethernet cable
  • A monitor and keyboard (or you can set up headless via SSH — covered below)

How It All Fits Together

At Home

                    THE INTERNET
                         |
                  ┌──────┴──────┐
                  |  ISP Router  |
                  |  (IoT/guest  |
                  |   WiFi only) |
                  └──┬───────┬──┘
                     |       |
              ┌──────┴──┐ ┌──┴──────────┐
              | Pi 3     | | Pi 5        |
              | Headscale| | (router)    |
              | server   | |             |
              └─────────┘ └──┬───────┬──┘
                             |       |
                          eth1    wlan0
                          (wired) (Wi-Fi)
                             |       |
                        ┌────┴──┐ ┌──┴────┐
                        |Wired  | |Phones |
                        |laptop | |tablets|
                        └───────┘ └───────┘

Your ISP router provides internet and handles only IoT gadgets and guest Wi-Fi. The Pi 5 connects to it for internet (via eth0) and creates a separate, private network for your trusted devices (via eth1 and wlan0). The Pi 3 runs the Headscale coordination server so all your Tailscale-enrolled devices can find each other.

On the Move

                    THE INTERNET
                         |
            ┌────────────┴────────────┐
            |                         |
     ┌──────┴──────┐          ┌──────┴──────┐
     | Hotel wired  |          | 4G/5G modem |
     | connection   |          | (backup)    |
     └──────┬──────┘          └──────┬──────┘
            |                         |
         eth0 (built-in)          wwan0 (USB)
            |                         |
     ┌──────┴─────────────────────────┴──────┐
     |                                        |
     |   Pi 5 (router) ─── Tailscale mesh ────── Pi 3 at home
     |   Pi-hole blocks ads & trackers        |   (Headscale)
     |   Offline services (Wikipedia, etc.)   |
     |                                        |
     └──────┬─────────────────────────┬──────┘
            |                         |
    eth1 (2.5G via HAT)       wlan0 (AX210 Wi-Fi)
            |                         |
     ┌──────┴──────┐          ┌──────┴──────┐
     | Wired device |          | Wi-Fi       |
     | (optional)   |          | devices     |
     └─────────────┘          └─────────────┘

When you travel, the Pi 5 connects to whatever internet is available (hotel ethernet or mobile data). Your devices connect to it exactly as they did at home. The Tailscale mesh reaches back to the Pi 3 at home through the internet — no special configuration needed, it just works.

Nothing is exposed to the public internet. The Pi 3 needs two port forwards on your ISP router (covered in the setup), but Headscale only responds to authenticated Tailscale clients — it’s not a web server anyone can browse to.


Part 1: Prepare the SSD

The Pi will boot and run entirely from the SSD plugged into its USB port. This is faster and more reliable than a microSD card.

Choose the Right USB-to-SATA Enclosure

Not all USB enclosures work properly with the Pi. You need one with a good controller chip inside. Look for enclosures that use one of these chips (usually listed in the product description or reviews):

  • ASMedia ASM1153E or ASM225CM — these work very well
  • Realtek RTL9210 — also good

Avoid enclosures with JMicron JMS578 or JMS567 chips — these have known issues with the Pi.

If you already have a Samsung T7 portable SSD, that works natively without needing an enclosure at all.

Write the Operating System to the SSD

  1. Download and install the Raspberry Pi Imager on your computer from raspberrypi.com/software
  2. Plug the SSD (in its enclosure) into your computer via USB
  3. In the Imager:
    • Device: Raspberry Pi 5
    • Operating System: Raspberry Pi OS Lite (64-bit) — under “Raspberry Pi OS (other)”
    • Storage: Select your SSD
  4. Click the settings cog before writing and configure:
    • Hostname: travelrouter
    • Enable SSH: Yes (password authentication)
    • Username: Pick one (e.g., admin)
    • Password: Pick a strong one
    • Wi-Fi: Leave this off (you’ll configure it manually later)
    • Locale: Set your timezone and keyboard layout
  5. Click Write and wait for it to finish

Set the Pi to Boot from USB

By default, the Pi tries to boot from a microSD card first. You need to tell it to look at USB instead:

  1. Write Raspberry Pi OS to a microSD card using the same Imager process above
  2. Put the microSD card in the Pi, connect a monitor and keyboard, and power it on
  3. Once it boots, open a terminal and run:
    sudo raspi-config
    
  4. Go to Advanced Options → Boot Order → USB Boot
  5. Select USB Boot and confirm
  6. Shut down the Pi:
    sudo shutdown -h now
    
  7. Remove the microSD card
  8. Plug in the SSD via USB and power the Pi back on — it should now boot from the SSD

Part 2: Assemble the Hardware

Put the Pi in Its Case

Follow the ZDE instructions that come with the ZP596 HAT and ZC506 case. The key steps are:

  1. Slot the Intel AX210 Wi-Fi card into the M.2 E-Key slot on the ZP596 board and secure it with the tiny screw
  2. Connect the two antenna cables that come with the HAT to the AX210 card (these are fiddly little snap connectors — press firmly until they click)
  3. Stack the ZP596 HAT onto the Pi 5’s GPIO header and connect the flat PCIe ribbon cable
  4. Slide everything into the ZC506 aluminium case and screw it together

Connect the SSD

Plug the SSD (in its USB enclosure) into one of the Pi’s USB 3.0 ports (the blue ones).

Connect the Mobile Modem (Optional)

If you’re using the Quectel 5G modem:

  1. Insert your SIM card into the modem module
  2. Slot the modem into the EXVIST adapter board
  3. Connect the adapter board to a USB port on the Pi
  4. Connect the external antennas if your adapter has antenna connectors

If you’re using the simpler Huawei E3372:

  1. Insert your SIM card
  2. Plug it into a USB port — that’s it

Part 3: Basic Network Setup

Now connect to your Pi and set up the network. Plug an Ethernet cable from your computer directly into the 2.5G port on the ZP596 HAT (this is the port added by the expansion board, not the built-in one).

Connect via SSH

From your computer’s terminal:

ssh admin@travelrouter.local

(Use whatever username you chose during setup. If .local doesn’t work, you may need to find the Pi’s IP address from your router’s admin page.)

Understand the Network Ports

Your Pi now has several network connections. Here’s what each one is for:

PortNameRole
Built-in Etherneteth0Internet in — plug this into a hotel/Airbnb wired connection
2.5G port on HATeth1Wired devices out — plug your laptop in here if you want a wired connection
AX210 Wi-Fiwlan0Wi-Fi hotspot — your devices connect to this wirelessly
USB modemwwan0Backup internet — mobile data kicks in if the wired connection drops

Update the System First

sudo apt update && sudo apt upgrade -y

Give the Local Network Port a Fixed Address

The port that your devices connect to needs a fixed address so it’s always reachable. Edit the DHCP client configuration:

sudo nano /etc/dhcpcd.conf

Add these lines at the bottom:

# Internet connection (wired) — get address automatically, prefer this route
interface eth0
metric 100

# Mobile data backup — get address automatically, use only if eth0 fails
interface wwan0
metric 600

# Local network port — fixed address, no internet gateway
interface eth1
static ip_address=10.0.0.1/24
nogateway

The metric numbers tell the Pi which internet connection to prefer. Lower numbers = higher priority. So it will always use the wired connection (100) first, and only fall back to mobile data (600) if the wired connection stops working.

Save the file (Ctrl+O, Enter, Ctrl+X) and reboot:

sudo reboot

Part 4: Set Up the Wi-Fi Hotspot

Rather than configuring everything by hand, you’ll use RaspAP — a web-based tool that makes managing the Wi-Fi hotspot, firewall, and VPN much simpler.

Install RaspAP

curl -sL https://install.raspap.com | bash

Follow the prompts. Say yes to:

  • Installing to the default /var/www/html location
  • Setting the default Wi-Fi country to your location (e.g., GB for UK)
  • Installing the ad-blocking feature

When it finishes, reboot:

sudo reboot

Configure Your Wi-Fi Network

  1. From a device connected to the Pi (via Ethernet to the 2.5G port), open a web browser and go to:
    http://10.0.0.1
    
  2. Log in with the default credentials:
    • Username: admin
    • Password: secret
  3. Change the admin password immediately — go to System → Authentication
  4. Go to Hotspot → Basic and configure:
    • Interface: wlan0
    • SSID: Pick a name for your network (e.g., TravelRouter)
    • Security: WPA2/WPA3
    • Password: Pick a strong Wi-Fi password
    • Country: Your country code
  5. Go to Hotspot → Advanced and set:
    • Wi-Fi mode: ac (or ax if your AX210 firmware supports it)
    • Channel: Pick a channel — 36 or 44 are usually good choices for the 5GHz band
  6. Save and restart the hotspot

You should now be able to see and connect to your Wi-Fi network from your phone or laptop.


Part 5: Set Up Ad-Blocking with Pi-hole

Pi-hole blocks adverts and tracking for every device on your network — without needing to install anything on each device.

Install Pi-hole

curl -sSL https://install.pi-hole.net | bash

During the setup wizard:

  • Upstream DNS: Choose any (e.g., Cloudflare at 1.1.1.1) — this will be routed through your VPN later
  • Interface: Select eth1
  • Static IP: It should detect 10.0.0.1 — confirm this
  • Web admin interface: Yes
  • Log queries: Your choice (useful for troubleshooting)

Set Pi-hole as the Network’s DNS and DHCP Server

  1. Open the Pi-hole admin panel at http://10.0.0.1/admin
  2. Go to Settings → DHCP and:
    • Enable DHCP server
    • Range: 10.0.0.50 to 10.0.0.150
    • Router (gateway): 10.0.0.1
  3. Disable DHCP in RaspAP (so they don’t fight each other):
    • Go to the RaspAP panel at http://10.0.0.1
    • Under DHCP Server, turn it off

Now Pi-hole handles handing out IP addresses and DNS for all your connected devices, and blocks ads in the process.


Part 6: Set Up the Pi 3 as Your Headscale Server

The Pi 3 stays at home permanently, plugged into your ISP router. It runs Headscale — a lightweight coordination server that lets all your Tailscale-connected devices find each other and communicate securely. Think of it as a private phone directory for your devices.

Headscale barely uses any resources — it runs happily on a Pi 3 with plenty to spare.

Prepare the Pi 3

  1. Write Raspberry Pi OS Lite (64-bit) to a microSD card using the Raspberry Pi Imager, just like you did for the Pi 5
    • Hostname: headscale
    • Enable SSH: Yes
    • Username/password: Pick something secure
  2. Insert the microSD card into the Pi 3, plug in Ethernet to your ISP router, and power it on
  3. SSH in from your computer:
    ssh admin@headscale.local
    
  4. Update everything:
    sudo apt update && sudo apt upgrade -y
    

Give the Pi 3 a Fixed Address

You need the Pi 3’s address on your home network to stay the same so the port forwarding (set up later) always points to the right place.

The easiest way is to do this in your ISP router’s admin page — look for DHCP reservation or static lease and assign a fixed IP to the Pi 3’s MAC address. For example, 192.168.1.200. (The exact steps vary by router — check your router’s manual.)

Install Docker on the Pi 3

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Log out and back in.

Set Up Dynamic DNS

Your home internet connection probably gets a different public IP address every now and then. Dynamic DNS gives your home a fixed name (like myhome.duckdns.org) that always points to your current IP, so the travelling Pi 5 can always find its way home.

Using DuckDNS (free):

  1. Go to duckdns.org and log in with a GitHub, Google, or other account
  2. Create a subdomain (e.g., mytravelrouter) — this gives you mytravelrouter.duckdns.org
  3. Note your token from the DuckDNS dashboard

On the Pi 3, set up automatic updates:

mkdir -p ~/duckdns
nano ~/duckdns/duck.sh
#!/bin/bash
echo url="https://www.duckdns.org/update?domains=mytravelrouter&token=YOUR-TOKEN-HERE&ip=" | curl -k -o ~/duckdns/duck.log -K -
chmod +x ~/duckdns/duck.sh

Add a cron job to run it every 5 minutes:

crontab -e

Add this line:

*/5 * * * * ~/duckdns/duck.sh >/dev/null 2>&1

Test it:

~/duckdns/duck.sh
cat ~/duckdns/duck.log

It should say OK.

Install Headscale

mkdir -p /opt/headscale/config /opt/headscale/data
nano /opt/headscale/docker-compose.yml
services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    restart: unless-stopped
    ports:
      - "8080:8080"
      - "3478:3478/udp"
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    command: serve

Create the configuration file:

nano /opt/headscale/config/config.yaml
server_url: https://mytravelrouter.duckdns.org
listen_addr: 0.0.0.0:8080
private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite
ip_prefixes:
  - 100.64.0.0/10
dns:
  magic_dns: true
  base_domain: tail.home
  nameservers.global:
    - 1.1.1.1
    - 9.9.9.9
derp:
  server:
    enabled: true
    region_id: 999
    stun_listen_addr: 0.0.0.0:3478

Start it:

cd /opt/headscale
docker compose up -d

Create a user for your devices:

docker exec headscale headscale users create travel

Set Up Port Forwarding on Your ISP Router

The Pi 5 needs to reach the Pi 3’s Headscale when you’re away from home. This means your ISP router needs to forward two ports to the Pi 3.

Log into your ISP router’s admin page and create these port forwards:

External PortInternal IPInternal PortProtocolPurpose
443192.168.1.200 (your Pi 3)8080TCPHeadscale coordination
3478192.168.1.200 (your Pi 3)3478UDPTailscale NAT traversal (STUN)

The exact steps differ by router, but it’s usually under Port Forwarding, NAT, or Virtual Server in the router’s settings.

Why port 443 externally? Many hotel and public Wi-Fi networks block unusual ports, but port 443 (the standard secure web port) is almost never blocked. Mapping it to Headscale’s port 8080 internally means it’ll work from almost anywhere.

Connect the Pi 5 to Headscale

Back on your Pi 5 (the travel router), install Tailscale:

curl -fsSL https://tailscale.com/install.sh | sh

Connect it to your Headscale server at home:

sudo tailscale up \
  --login-server https://mytravelrouter.duckdns.org \
  --advertise-exit-node \
  --advertise-routes=10.0.0.0/24 \
  --accept-dns=false

This tells the Pi 5 to:

  • Connect to your Headscale server on the Pi 3 at home
  • Offer itself as an exit node (meaning other Tailscale devices can route all their internet through the Pi 5)
  • Share the local 10.0.0.0/24 network so remote Tailscale devices can access the offline services

On the Pi 3, approve the Pi 5:

docker exec headscale headscale nodes register --user travel --key <the-key-shown-on-the-pi-5>
docker exec headscale headscale routes enable --route 10.0.0.0/24 --identifier <node-id>
docker exec headscale headscale nodes update --identifier <node-id> --exit-node

Connect Your Phone and Laptop

Install the Tailscale app on each device you want to use with the router:

  • Android/iOS: Install from the app store, then go to Settings and change the coordination server to https://mytravelrouter.duckdns.org
  • Windows/Mac/Linux: Install Tailscale, then run: tailscale up --login-server https://mytravelrouter.duckdns.org

On the Pi 3, approve each device:

docker exec headscale headscale nodes register --user travel --key <the-key-shown-on-the-device>

Once connected, your devices are part of the Tailscale mesh. They can reach the Pi 5’s services (Wikipedia, Jellyfin, etc.) from anywhere — at home, in a hotel, or on the other side of the world — all encrypted, all private.

The key point: Tailscale handles all the complexity of devices moving between networks, being behind firewalls, and NAT traversal. It uses WireGuard encryption under the hood. You don’t need to worry about any of that. Devices just find each other.


Part 7: Automatic Internet Failover

This is the clever bit. You’ll create a small script that constantly checks whether the wired internet connection is working. If it drops out, the Pi automatically switches to mobile data. When the wired connection comes back, it switches back.

Create the Failover Script

sudo nano /usr/local/bin/wan-failover.sh
#!/bin/bash
# Servers to test — if we can reach any of these, the internet is working
TEST_HOSTS=("1.1.1.1" "8.8.8.8" "9.9.9.9")
FAILURES=0
MAX_FAILURES=3

while true; do
    # Try to reach each test server through the wired connection
    SUCCESS=false
    for host in "${TEST_HOSTS[@]}"; do
        if ping -c 1 -W 2 -I eth0 "$host" &>/dev/null; then
            SUCCESS=true
            break
        fi
    done

    if $SUCCESS; then
        FAILURES=0
        # Make sure we're using the wired connection (lower metric = preferred)
        if ip route show default | grep -q "wwan0.*metric 100"; then
            echo "$(date): Wired connection restored, switching back"
            sudo ip route del default dev wwan0 metric 100 2>/dev/null
            sudo ip route add default dev eth0 metric 100 2>/dev/null
        fi
    else
        FAILURES=$((FAILURES + 1))
        if [ $FAILURES -ge $MAX_FAILURES ]; then
            echo "$(date): Wired connection failed $MAX_FAILURES times, switching to mobile"
            sudo ip route del default dev eth0 metric 100 2>/dev/null
            sudo ip route add default dev wwan0 metric 100 2>/dev/null
        fi
    fi

    sleep 10
done

Make it executable:

sudo chmod +x /usr/local/bin/wan-failover.sh

Make It Start Automatically

Create a systemd service so the failover script runs in the background:

sudo nano /etc/systemd/system/wan-failover.service
[Unit]
Description=WAN Failover Monitor
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/wan-failover.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable and start it:

sudo systemctl enable --now wan-failover.service

Now the Pi will automatically switch between wired and mobile internet without you having to do anything.


Part 8: Set Up the Firewall

The firewall ensures that your devices’ traffic is handled properly and nothing unexpected gets through. Since all remote access goes through Tailscale, the firewall is straightforward — allow local device traffic and Tailscale, block everything else from reaching your services directly. This uses nftables, which comes pre-installed on Raspberry Pi OS.

sudo nano /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Allow traffic from your own local devices
        iifname { "eth1", "wlan0" } accept

        # Allow responses to things the Pi itself requested
        ct state established,related accept

        # Allow Tailscale mesh traffic
        iifname "tailscale0" accept

        # Allow local loopback
        iifname "lo" accept

        # Allow DHCP on the internet-facing port
        iifname "eth0" udp dport 68 accept
        iifname "wwan0" udp dport 68 accept

        # Allow Tailscale's WireGuard port (41641/udp by default)
        iifname { "eth0", "wwan0" } udp dport 41641 accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        # Allow local devices out to the internet
        iifname { "eth1", "wlan0" } oifname { "eth0", "wwan0" } accept

        # Allow Tailscale traffic to/from local network
        iifname "tailscale0" oifname { "eth1", "wlan0" } accept
        iifname { "eth1", "wlan0" } oifname "tailscale0" accept

        # Allow responses back in
        iifname { "eth0", "wwan0" } oifname { "eth1", "wlan0" } ct state established,related accept
    }
}

table ip nat {
    chain postrouting {
        type nat hook postrouting priority 100;
        # Masquerade (share the internet connection with your devices)
        oifname { "eth0", "wwan0" } masquerade
    }
}

Enable the firewall:

sudo systemctl enable --now nftables

Enable IP forwarding (this lets the Pi pass traffic between your devices and the internet):

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-forward.conf
sudo sysctl -p /etc/sysctl.d/99-forward.conf

What this achieves: your local devices (connected via Wi-Fi or the wired port) can reach the internet and each other. Tailscale-authenticated remote devices can reach your local services. Nothing from the upstream network (hotel, ISP) can reach your devices or services directly — the only door in is through Tailscale.


Part 9: Offline Services

This is where it gets fun. You’ll set up a collection of services that work entirely without an internet connection — useful on planes, trains, ferries, campsites, or anywhere with poor signal.

Install Docker

Docker lets you run each service in its own isolated container, making them easy to install, update, and remove.

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Log out and back in for the group change to take effect.

Create the Services Configuration

mkdir -p /home/admin/services
nano /home/admin/services/docker-compose.yml
services:
  # OFFLINE WIKIPEDIA, BOOKS & REFERENCE
  kiwix:
    image: ghcr.io/kiwix/kiwix-serve:latest
    container_name: kiwix
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - /mnt/ssd/kiwix:/data
    command: /data/*.zim

  # MUSIC & VIDEO PLAYER
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    ports:
      - "8096:8096"
    volumes:
      - /mnt/ssd/jellyfin/config:/config
      - /mnt/ssd/jellyfin/cache:/cache
      - /mnt/ssd/media:/media:ro
    environment:
      - JELLYFIN_PublishedServerUrl=http://10.0.0.1:8096

  # OFFLINE MAPS
  tileserver:
    image: maptiler/tileserver-gl:latest
    container_name: tileserver
    restart: unless-stopped
    ports:
      - "8081:8080"
    volumes:
      - /mnt/ssd/maps:/data

  # FILE SHARING BETWEEN DEVICES
  filebrowser:
    image: filebrowser/filebrowser:latest
    container_name: filebrowser
    restart: unless-stopped
    ports:
      - "8082:80"
    volumes:
      - /mnt/ssd/shared:/srv
      - /mnt/ssd/filebrowser/database.db:/database.db
    environment:
      - FB_NOAUTH=false

Set Up the Storage Folders

# Create a mount point for the SSD
sudo mkdir -p /mnt/ssd

# Find your SSD's partition (usually /dev/sda1 or /dev/sda2)
lsblk

# Add it to auto-mount on boot
echo '/dev/sda2 /mnt/ssd ext4 defaults,noatime 0 2' | sudo tee -a /etc/fstab
sudo mount -a

# Create the folder structure
sudo mkdir -p /mnt/ssd/{kiwix,jellyfin/config,jellyfin/cache,media/music,media/films,media/tv,maps,shared,filebrowser}
sudo chown -R $USER:$USER /mnt/ssd

Download Offline Content

Wikipedia and Reference

Download pre-packaged files from the Kiwix library. Each file is self-contained and just needs to be dropped into the folder.

Browse the full catalogue at: download.kiwix.org/zim/

Recommended downloads for a travel setup:

ContentSizeFilename to look for
Wikipedia (English, with pictures)~109 GBwikipedia_en_all_maxi_*.zim
Wikipedia (English, no pictures)~12 GBwikipedia_en_all_nopic_*.zim
WikiMed (medical encyclopaedia)~1 GBwikipedia_en_medicine_*.zim
Stack Overflow~28 GBstackoverflow.com_en_all_*.zim
Project Gutenberg (70,000+ books)~75 GBgutenberg_en_all_*.zim
iFixit repair guides~3 GBifixit_en_all_*.zim
Wiktionary (dictionary)~7 GBwiktionary_en_all_maxi_*.zim
TED Talks~20 GBted_en_all_*.zim

Download them directly on the Pi to save transferring large files:

cd /mnt/ssd/kiwix
wget https://download.kiwix.org/zim/wikipedia/wikipedia_en_all_nopic_2024-10.zim

Start with the smaller no pictures version of Wikipedia if you want to save space — you can always download the full version later.

Maps

Download map tiles from OpenMapTiles in MBTiles format.

Suggested sizes:

  • United Kingdom: ~1.5 GB
  • Germany: ~3 GB
  • All of Europe: ~20 GB

Place the .mbtiles files in /mnt/ssd/maps/.

Alternatively, Kiwix also offers offline OpenStreetMap as .zim files, which work through the Kiwix interface.

Music and Video

Copy your music and video files to /mnt/ssd/media/music/ and /mnt/ssd/media/films/.

For the best experience on the Pi (which can’t convert video formats on the fly), pre-convert your videos to a widely compatible format on your main computer before copying them over:

ffmpeg -i input.mkv \
  -c:v libx264 -preset slow -crf 20 -profile:v high -level 4.1 \
  -c:a aac -b:a 192k -ac 2 \
  -c:s srt \
  -movflags +faststart \
  output.mp4

This creates files that virtually any phone, tablet, or laptop can play directly without the Pi needing to do any conversion work. Each hour of 1080p video takes roughly 4–8 GB.

Start Everything

cd /home/admin/services
docker compose up -d

Create a Simple Home Page

Rather than remembering port numbers, create a landing page that links to everything:

sudo mkdir -p /var/www/html
sudo nano /var/www/html/index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Travel Router</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
        h1 { text-align: center; margin-bottom: 2rem; font-size: 1.8rem; }
        .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; max-width: 800px; margin: 0 auto; }
        a { text-decoration: none; color: inherit; }
        .card { background: #1e293b; border-radius: 12px; padding: 1.5rem; transition: transform 0.2s; }
        .card:hover { transform: translateY(-2px); background: #334155; }
        .card h2 { font-size: 1.2rem; margin-bottom: 0.5rem; }
        .card p { font-size: 0.9rem; color: #94a3b8; }
    </style>
</head>
<body>
    <h1>Travel Router</h1>
    <div class="grid">
        <a href="http://10.0.0.1:8080"><div class="card">
            <h2>Wikipedia & Books</h2>
            <p>Offline encyclopaedia, e-books, repair guides, and more</p>
        </div></a>
        <a href="http://10.0.0.1:8096"><div class="card">
            <h2>Music & Video</h2>
            <p>Stream your film and music library to any device</p>
        </div></a>
        <a href="http://10.0.0.1:8081"><div class="card">
            <h2>Offline Maps</h2>
            <p>Browse detailed maps without an internet connection</p>
        </div></a>
        <a href="http://10.0.0.1:8082"><div class="card">
            <h2>File Sharing</h2>
            <p>Upload and download files between your devices</p>
        </div></a>
        <a href="http://10.0.0.1/admin"><div class="card">
            <h2>Ad Blocker</h2>
            <p>Pi-hole dashboard — see what's being blocked</p>
        </div></a>
        <a href="http://10.0.0.1"><div class="card">
            <h2>Router Settings</h2>
            <p>RaspAP — manage Wi-Fi, VPN, and network settings</p>
        </div></a>
    </div>
</body>
</html>

Install nginx to serve it:

sudo apt install -y nginx

Edit the nginx config to listen on port 80 and serve your landing page:

sudo nano /etc/nginx/sites-available/default

Make sure the root line points to /var/www/html and the server block listens on port 80. The default config usually works fine.

Restart nginx:

sudo systemctl restart nginx

Now anyone connected to your network — either locally via Wi-Fi/Ethernet, or remotely via Tailscale — can open a browser, go to http://10.0.0.1, and see a clean menu of everything available.


Part 10: Verify Everything Works

Check Your USB SSD is Using the Fast Driver

lsusb -t

Look for your SSD in the output. It should show uas as the driver, not usb-storage. If it shows usb-storage, your enclosure’s chip isn’t fully compatible and you’ll get slower speeds.

Check All Services Are Running

docker ps

You should see kiwix, jellyfin, tileserver, and filebrowser all listed as Up.

Check the Tailscale Mesh Is Working

tailscale status

You should see your Pi 5 listed, and any other devices you’ve enrolled. From a Tailscale-connected device away from home, try accessing http://10.0.0.1 — if you can see the landing page, the mesh is working.

Test the Failover

  1. With an Ethernet cable plugged into eth0, confirm you have internet
  2. Unplug the cable
  3. Wait about 30 seconds
  4. Check if you still have internet (now through mobile data)
  5. Plug the cable back in
  6. Wait about 10 seconds — it should switch back to wired

Check Memory Usage

free -h

All the services together use roughly 1 GB of the Pi’s 8 GB of RAM, leaving plenty of headroom.


Storage Budget

Here’s roughly how the 2 TB SSD breaks down:

ContentSpace Used
Operating system, Docker, and services~10 GB
Wikipedia (no pictures) + WikiMed + Wiktionary~20 GB
Stack Overflow + iFixit + Gutenberg e-books~106 GB
Maps (UK + major European countries)~20 GB
Music library~300 GB
Video library (~150 hours of 1080p)~800 GB
Shared file storage~100 GB
Free space remaining~650 GB

If you go with the full Wikipedia including pictures (109 GB), that eats into your free space but you’ll still have plenty of room.


Quick Reference

Once everything is set up, here’s what you need to know day-to-day:

TaskHow
Connect to Wi-FiJoin the network you named (e.g., TravelRouter)
Browse offline contentOpen http://10.0.0.1 in any browser
Manage the routerOpen http://10.0.0.1 → Router Settings
Check ad-blocking statsOpen http://10.0.0.1/admin
SSH into the Pi 5ssh admin@10.0.0.1
SSH into the Pi 3 (from home)ssh admin@headscale.local
SSH into the Pi 3 (from anywhere)ssh admin@100.64.0.X (Tailscale IP)
Check Tailscale meshtailscale status
Run a manual backupsudo /usr/local/bin/backup-config.sh
Run a manual content syncsudo /usr/local/bin/sync-to-pi3.sh (home network only)
Restart all servicescd ~/services && docker compose restart
Check what’s runningdocker ps
Update servicescd ~/services && docker compose pull && docker compose up -d
Safely shut downsudo shutdown -h now

Troubleshooting

Can’t see the Wi-Fi network? SSH in via Ethernet and check if hostapd is running: sudo systemctl status hostapd. Try restarting it: sudo systemctl restart hostapd.

Can’t reach services remotely via Tailscale? Check tailscale status on the Pi 5. If it shows as offline, the Pi may not be able to reach the Headscale server on the Pi 3 at home. Check that the Pi 3 is running (ssh admin@headscale.local), that the DuckDNS name resolves (ping mytravelrouter.duckdns.org), and that the port forwards on your ISP router are correct (443 → Pi 3:8080, 3478/udp → Pi 3:3478).

SSD not detected? Try a different USB port (use the blue USB 3.0 ports). Check lsblk to see if the drive appears. If not, the enclosure may not be compatible — check the controller chip.

Mobile data not connecting? Check ip a to see if wwan0 exists. If not, the modem may need additional drivers. For the Huawei E3372, check if it appears as a network device with lsusb. For the Quectel modem, you may need to install modemmanager: sudo apt install modemmanager.

Pi-hole not blocking ads? Make sure Pi-hole’s DHCP server is enabled and RaspAP’s is disabled. Check that connected devices are getting 10.0.0.1 as their DNS server: look at your device’s network settings.


Part 11: Backup and Disaster Recovery

The travel router carries a lot of configuration that would be tedious to redo from scratch, plus potentially over a terabyte of content that took days to download and organise. Here’s how to protect against an SSD failure, a dead Pi, or the whole thing getting stolen — with a strategy that backs up everything, not just config.

What Matters and What Doesn’t

Think of the data on your SSD in two categories:

CategoryExamplesCan you get it back?
Configuration (small, irreplaceable)Tailscale identity, Wi-Fi passwords, firewall rules, Docker compose file, Pi-hole settings, failover scriptOnly if you backed it up
Content (large, replaceable but tedious)Wikipedia ZIM files, maps, music, films, e-booksCan re-download, but the Pi 3 mirror means you won’t have to

The backup strategy focuses on the first category. There’s no point backing up 800 GB of films when you already have them on your home computer.

The Two-Tier Backup Strategy

You have two backup locations, each protecting against different problems:

BackupWhereWhatProtects Against
Local — microSD card inside the Pi 5Travels with the routerConfiguration only (a few MB)SSD failure
Remote — external drive on the Pi 3 at homeStays at homeFull mirror of the entire SSD — config, content, Docker volumes, everythingTheft, fire, total loss of the Pi 5

The microSD card you used for the initial USB boot setup is already sitting in the Pi 5 doing nothing — it becomes your quick-recovery config backup. The external drive on the Pi 3 mirrors the whole SSD, so you can restore a replacement in one go — no re-downloading, no re-copying, no piecing things back together.

Prepare the MicroSD Card as a Backup Drive

The microSD still has the initial Raspberry Pi OS on it from the boot setup. You’ll reformat it as a simple storage drive:

# Find the microSD — it's usually /dev/mmcblk0
lsblk

# Wipe it and create a single partition
sudo parted /dev/mmcblk0 --script mklabel gpt mkpart primary ext4 0% 100%

# Format it
sudo mkfs.ext4 -L pibackup /dev/mmcblk0p1

# Create a mount point and set it to mount automatically
sudo mkdir -p /mnt/sdbackup
echo '/dev/mmcblk0p1 /mnt/sdbackup ext4 defaults,noatime 0 2' | sudo tee -a /etc/fstab
sudo mount -a

# Create the backup folder
sudo mkdir -p /mnt/sdbackup/backups
sudo chown -R $USER:$USER /mnt/sdbackup

Prepare the External Drive on the Pi 3

Plug a USB hard drive (2 TB or larger) into the Pi 3 at home. This will hold a full mirror of the Pi 5’s entire SSD — OS files, config, content, Docker volumes, custom scripts, everything — so if the worst happens, you can restore a replacement drive in one go.

A portable USB hard drive is fine for this. It doesn’t need to be fast — the Pi 3 only has USB 2.0 anyway, and the sync runs overnight.

On the Pi 3:

# Find the drive
lsblk

# Format it (adjust /dev/sda1 to match your drive)
sudo mkfs.ext4 -L pi3backup /dev/sda1

# Mount it
sudo mkdir -p /mnt/backup
echo '/dev/sda1 /mnt/backup ext4 defaults,noatime 0 2' | sudo tee -a /etc/fstab
sudo mount -a

# Create folders
sudo mkdir -p /mnt/backup/pi5-config /mnt/backup/pi5-full /mnt/backup/pi3-headscale
sudo chown -R $USER:$USER /mnt/backup

Create the Backup Script (Pi 5)

This script saves the Pi 5’s configuration to both the local microSD and the remote Pi 3:

sudo nano /usr/local/bin/backup-config.sh
#!/bin/bash
DATE=$(date +%Y-%m-%d)
ARCHIVE_NAME="travelrouter-config-$DATE.tar.gz"

# === Step 1: Create the config archive ===
echo "Backing up travel router configuration..."

TEMP_ARCHIVE="/tmp/$ARCHIVE_NAME"
tar czf "$TEMP_ARCHIVE" \
  /etc/dhcpcd.conf \
  /etc/nftables.conf \
  /etc/sysctl.d/99-forward.conf \
  /etc/hostapd/ \
  /etc/dnsmasq.d/ \
  /etc/pihole/ \
  /etc/nginx/sites-available/ \
  /var/www/html/index.html \
  /usr/local/bin/wan-failover.sh \
  /usr/local/bin/backup-config.sh \
  /usr/local/bin/sync-to-pi3.sh \
  /etc/systemd/system/wan-failover.service \
  /etc/systemd/system/backup-config.service \
  /etc/systemd/system/backup-config.timer \
  /etc/systemd/system/sync-to-pi3.service \
  /etc/systemd/system/sync-to-pi3.timer \
  /home/admin/services/docker-compose.yml \
  /home/admin/.ssh/ \
  /var/lib/tailscale/ \
  /etc/fstab \
  2>/dev/null

SIZE=$(du -h "$TEMP_ARCHIVE" | cut -f1)

# === Step 2: Local backup to microSD ===
if mountpoint -q /mnt/sdbackup; then
    cp "$TEMP_ARCHIVE" /mnt/sdbackup/backups/
    # Keep only the last 10 on the SD card
    ls -t /mnt/sdbackup/backups/travelrouter-config-*.tar.gz 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null
    echo "Local backup saved to microSD ($SIZE)"
else
    echo "WARNING: microSD not mounted — local backup skipped"
fi

# === Step 3: Remote config backup to Pi 3 ===
# Use the Pi 3's Tailscale IP so this works both at home and when travelling
PI3_TAILSCALE_IP="100.64.0.X"  # Replace with your Pi 3's actual Tailscale IP

if tailscale ping --timeout=3s "$PI3_TAILSCALE_IP" &>/dev/null; then
    scp "$TEMP_ARCHIVE" admin@"$PI3_TAILSCALE_IP":/mnt/backup/pi5-config/ 2>/dev/null && \
        echo "Remote config backup sent to Pi 3" || \
        echo "Remote config backup failed (SCP error)"
    # Clean up old remote backups (keep last 10)
    ssh admin@"$PI3_TAILSCALE_IP" 'ls -t /mnt/backup/pi5-config/travelrouter-config-*.tar.gz 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null'
else
    echo "Pi 3 not reachable — remote config backup skipped (will retry tomorrow)"
fi

# Clean up
rm -f "$TEMP_ARCHIVE"
echo "Done."
sudo chmod +x /usr/local/bin/backup-config.sh

Create the Full Sync Script (Pi 5)

This is a separate script that mirrors the entire SSD to the Pi 3’s external drive — content, Docker volumes, home directory, everything. It only runs when the Pi 5 is on the local home network — syncing hundreds of gigabytes over a Tailscale connection from a hotel would be impractical.

Rather than listing individual folders (and inevitably missing something), it syncs everything and excludes only the things that don’t need backing up.

sudo nano /usr/local/bin/sync-to-pi3.sh
#!/bin/bash
PI3_LOCAL_IP="192.168.1.200"  # Replace with your Pi 3's local IP on your home network

# Only sync if we're on the home network (can reach Pi 3 directly)
if ! ping -c 1 -W 2 "$PI3_LOCAL_IP" &>/dev/null; then
    echo "$(date): Not on home network — full sync skipped"
    exit 0
fi

echo "$(date): On home network — starting full sync to Pi 3..."

# Mirror the entire SSD, excluding things that don't need backing up
rsync -az --delete --partial --info=progress2 \
    --exclude='/proc/*' \
    --exclude='/sys/*' \
    --exclude='/dev/*' \
    --exclude='/run/*' \
    --exclude='/tmp/*' \
    --exclude='/var/tmp/*' \
    --exclude='/var/log/*' \
    --exclude='/var/cache/*' \
    --exclude='/var/lib/apt/*' \
    --exclude='/var/lib/docker/overlay2/*' \
    --exclude='/var/lib/docker/containers/*/logs/*' \
    --exclude='/swap*' \
    --exclude='/lost+found' \
    --exclude='.cache' \
    --exclude='__pycache__' \
    / \
    admin@"$PI3_LOCAL_IP":/mnt/backup/pi5-full/

echo "$(date): Full sync complete"
sudo chmod +x /usr/local/bin/sync-to-pi3.sh

What this catches that the old approach missed:

  • Docker volumesJellyfin’s library database, watch history, thumbnails, and metadata. Pi-hole’s long-term query database. FileBrowser’s user settings.
  • Home directory — any scripts, notes, or files you’ve created on the Pi
  • Custom system configs — anything you’ve tweaked that isn’t in the config backup’s list
  • New services — if you add another Docker service later, its data gets backed up automatically without changing the script

What’s excluded and why:

  • /proc, /sys, /dev, /run — virtual filesystems, not real files
  • /var/log, /var/cache, /var/lib/apt — logs and caches that rebuild themselves
  • /var/lib/docker/overlay2 — Docker image layers (re-pulled with docker compose pull)
  • Docker container logs — regenerated when containers restart
  • Swap files, .cache directories — temporary data

Why the local IP instead of Tailscale? Speed. Over the local network, rsync can push data at the Pi 3’s USB 2.0 limit (~30 MB/s, roughly 100 GB per hour). Over Tailscale, even at home, there’s extra overhead. And you definitely don’t want this running over a hotel’s internet connection.

How rsync works: The first sync copies everything — this will take a while depending on how much data you have (a full 1 TB at 30 MB/s takes roughly 9 hours). After that, rsync only sends files that have changed, so nightly syncs are fast unless you’ve added a lot of new media.

Important: Replace 100.64.0.X with your Pi 3’s actual Tailscale IP (find it with tailscale status). Replace 192.168.1.200 with the Pi 3’s actual local IP on your home network. The config backup uses Tailscale so it works from anywhere; the full sync uses the local IP so it’s fast.

SSH key setup: For both scripts to work without asking for a password, set up key-based SSH between the Pi 5 and Pi 3:

# On the Pi 5, generate a key (press Enter for all prompts)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""

# Copy it to the Pi 3 (do both addresses so it works either way)
ssh-copy-id admin@100.64.0.X
ssh-copy-id admin@192.168.1.200

Create the Backup Script (Pi 3)

The Pi 3’s Headscale data is equally important — without it, none of your devices can find each other. Create a similar backup script on the Pi 3:

sudo nano /usr/local/bin/backup-headscale.sh
#!/bin/bash
DATE=$(date +%Y-%m-%d)
ARCHIVE="/mnt/backup/pi3-headscale/headscale-config-$DATE.tar.gz"

echo "Backing up Headscale and Pi 3 configuration..."

tar czf "$ARCHIVE" \
  /opt/headscale/ \
  /usr/local/bin/backup-headscale.sh \
  /etc/systemd/system/backup-headscale.service \
  /etc/systemd/system/backup-headscale.timer \
  /etc/fstab \
  /home/admin/duckdns/ \
  /var/spool/cron/crontabs/ \
  2>/dev/null

# Keep only the last 10 backups
ls -t /mnt/backup/pi3-headscale/headscale-config-*.tar.gz 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null

SIZE=$(du -h "$ARCHIVE" | cut -f1)
echo "Done. Headscale backup saved ($SIZE)"
sudo chmod +x /usr/local/bin/backup-headscale.sh

Run Everything Automatically

On the Pi 5 — config backup (runs nightly, works anywhere):

sudo nano /etc/systemd/system/backup-config.timer
[Unit]
Description=Nightly configuration backup

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo nano /etc/systemd/system/backup-config.service
[Unit]
Description=Backup travel router configuration

[Service]
Type=oneshot
User=admin
ExecStart=/usr/local/bin/backup-config.sh
sudo systemctl enable --now backup-config.timer

On the Pi 5 — full sync (runs nightly, only does anything when at home):

sudo nano /etc/systemd/system/sync-to-pi3.timer
[Unit]
Description=Nightly full sync to Pi 3

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo nano /etc/systemd/system/sync-to-pi3.service
[Unit]
Description=Sync everything to Pi 3 backup drive

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/sync-to-pi3.sh
# Give rsync up to 8 hours for large initial syncs
TimeoutStopSec=28800
sudo systemctl enable --now sync-to-pi3.timer

The full sync runs at 2 AM, an hour before the config backup. When you’re travelling, it detects it’s not on the home network and exits immediately. When you’re at home, it syncs any new or changed files. You can also run it manually at any time with sudo /usr/local/bin/sync-to-pi3.sh.

On the Pi 3:

sudo nano /etc/systemd/system/backup-headscale.timer
[Unit]
Description=Nightly Headscale backup

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target
sudo nano /etc/systemd/system/backup-headscale.service
[Unit]
Description=Backup Headscale configuration

[Service]
Type=oneshot
User=admin
ExecStart=/usr/local/bin/backup-headscale.sh
sudo systemctl enable --now backup-headscale.timer

What’s Backed Up Where

After the nightly jobs run, here’s what’s stored where:

LocationContainsSurvives
Pi 5 microSD (/mnt/sdbackup/backups/)Last 10 days of Pi 5 config (a few MB)SSD failure
Pi 3 external drive (/mnt/backup/pi5-config/)Last 10 days of Pi 5 configPi 5 stolen, SSD failure
Pi 3 external drive (/mnt/backup/pi5-full/)Full mirror of the Pi 5’s SSD — OS, config, content, Docker volumes, everythingPi 5 stolen, SSD failure
Pi 3 external drive (/mnt/backup/pi3-headscale/)Last 10 days of Headscale configPi 3 SD card failure

If the SSD Dies

This is the most likely failure. You have two recovery paths depending on where you are.

Option A: Full restore at home (easiest — the Pi 3 has everything)

Since the Pi 3’s external drive has a full mirror of the SSD, you can restore the entire system in one go — OS files, config, content, Docker volumes, everything.

  1. Get a new SSD and write Raspberry Pi OS Lite to it (Part 1 of this guide)
  2. Boot the Pi and do the basic setup (connect to your home network, enable SSH)
  3. Pull the full mirror back from the Pi 3:
    # Restore everything from the Pi 3's mirror
    sudo rsync -az --info=progress2 \
        admin@192.168.1.200:/mnt/backup/pi5-full/ \
        /
    
    # Reboot to pick up all the restored configs
    sudo reboot
    
  4. Reinstall the software (rsync restores config files and data, but not the installed packages themselves):
    # Install Docker
    curl -fsSL https://get.docker.com | sh
    sudo usermod -aG docker $USER
    
    # Install Pi-hole
    curl -sSL https://install.pi-hole.net | bash
    
    # Install RaspAP
    curl -sL https://install.raspap.com | bash
    
    # Install Tailscale
    curl -fsSL https://tailscale.com/install.sh | sh
    
    # Install nginx
    sudo apt install -y nginx
    
    # Enable the firewall and failover service
    sudo systemctl enable --now nftables
    sudo systemctl enable --now wan-failover.service
    
    # Re-enable the backup and sync timers (the files are already restored)
    sudo systemctl enable --now backup-config.timer
    sudo systemctl enable --now sync-to-pi3.timer
    
    # Start Docker services (images will re-pull, but volumes are already restored)
    cd /home/admin/services
    docker compose up -d
    
  5. Set the microSD back up as a backup drive (re-format it as described earlier)

Everything comes back — config, Kiwix files, maps, media, Jellyfin library and watch history, Pi-hole blocklists and query data, shared files, custom scripts, backup and sync automation, the lot.

Option B: Quick restore while travelling (microSD only)

If the SSD dies while you’re away from home, the microSD inside the Pi still has your config backups.

  1. Get a new SSD and write Raspberry Pi OS Lite to it
  2. Mount the microSD and restore config:
    sudo mkdir -p /mnt/sdbackup
    sudo mount /dev/mmcblk0p1 /mnt/sdbackup
    ls -la /mnt/sdbackup/backups/
    sudo tar xzf /mnt/sdbackup/backups/travelrouter-config-2026-02-13.tar.gz -C /
    
  3. Reinstall the software (same as step 4 above)

This gets the router working — Wi-Fi, ad-blocking, firewall, Tailscale — but without your content or Docker volume data. When you’re back home, pull the full mirror back from the Pi 3:

sudo rsync -az --info=progress2 \
    admin@192.168.1.200:/mnt/backup/pi5-full/ \
    /
sudo reboot

If the Whole Device Is Stolen (or Destroyed)

The microSD goes with the Pi 5, so if the whole thing is gone, you need the Pi 3’s backup. The good news: it has a full mirror of everything.

  1. New hardware — same parts list from the top of this guide
  2. Set up the new Pi 5 and SSD — write Raspberry Pi OS Lite, boot, enable SSH (Parts 1–2)
  3. Restore the full mirror from the Pi 3:
    # Pull everything back
    sudo rsync -az --info=progress2 \
        admin@192.168.1.200:/mnt/backup/pi5-full/ \
        /
    sudo reboot
    
  4. Reinstall the software (same as Option A, step 4)
  5. Revoke the stolen device’s Tailscale node immediately so it can’t connect to your network:
    # SSH into the Pi 3
    docker exec headscale headscale nodes delete --identifier <stolen-node-id>
    

Everything comes back — config, content, Docker volumes, Jellyfin history, the lot. Then change your Wi-Fi password, RaspAP admin password, and Pi-hole admin password on the new build (the thief knows the old ones).

If the Pi 3 Dies

The Pi 3 is simpler to rebuild since it only runs Headscale:

  1. Flash a new microSD with Raspberry Pi OS Lite
  2. Install Docker and restore from the external drive:
    # The external drive still has the backup — just plug it in
    sudo mkdir -p /mnt/backup
    sudo mount /dev/sda1 /mnt/backup
    ls -la /mnt/backup/pi3-headscale/
    sudo tar xzf /mnt/backup/pi3-headscale/headscale-config-2026-02-13.tar.gz -C /
    
    curl -fsSL https://get.docker.com | sh
    cd /opt/headscale
    docker compose up -d
    
    # Re-enable the backup timer (the files are already restored)
    sudo systemctl enable --now backup-headscale.timer
    
  3. Restart the cron service so the restored DuckDNS cron job takes effect:
    sudo systemctl restart cron
    
  4. Check the port forwards on your ISP router are still pointing to the Pi 3’s IP

All your enrolled Tailscale devices will reconnect automatically once Headscale is back up.

Keep a Rebuild Cheat Sheet

Save a simple text file in two places — on the Pi 3’s external drive and on your phone — listing:

  • Your Pi 3’s local IP, Tailscale IP, and SSH credentials
  • Your DuckDNS subdomain and token
  • Your Headscale user name
  • Any custom Pi-hole blocklists you added

Since the Pi 3’s drive has a full mirror of your content, you no longer need to track download URLs or remember where files came from. Everything restores from one place.

Your config backups contain Tailscale private keys and Wi-Fi passwords. The microSD inside the Pi 5 is reasonably safe (someone would need to physically disassemble the case), but the remote copies on the Pi 3’s external drive are worth encrypting.

Add this to both backup scripts, after creating the archive:

# Set up once on each Pi:
echo "your-backup-passphrase" | sudo tee /root/.backup-passphrase
sudo chmod 600 /root/.backup-passphrase
# Add to the backup script, after creating the .tar.gz:
gpg --batch --yes --symmetric --cipher-algo AES256 \
    --passphrase-file /root/.backup-passphrase \
    "$ARCHIVE"
rm -f "$ARCHIVE"  # Remove the unencrypted version

# The .gpg file is what gets kept/sent

# To decrypt later:
gpg --batch --passphrase-file /root/.backup-passphrase \
    --decrypt travelrouter-config-2026-02-13.tar.gz.gpg > travelrouter-config-2026-02-13.tar.gz

Where to Go Next

  • Add more Kiwix content: Browse the full library at download.kiwix.org/zim/ — there’s everything from Wikivoyage travel guides to Khan Academy educational videos
  • Encrypt the SSD at rest: If you’re worried about someone accessing your data after theft, look into LUKS full-disk encryption (requires entering a password at every boot)
  • Set up Headscale UI: Install headscale-ui on the Pi 3 for a web-based dashboard to manage your Tailscale nodes instead of using the command line
  • Add a UPS for the Pi 3: A small USB UPS (like a PiSugar or similar) keeps the Pi 3 running through short power cuts, so Headscale stays available for your travelling devices
Axel Segebrecht

Founder of Aerouant and advocate for community action, Axel Segebrecht explores the practical side of climate resilience. By documenting his journey, Axel provides a roadmap for those looking to exit the business-as-usual cycle. He believes the path to a solarpunk reality starts at the local level: protecting our neighbourhoods through shared energy, local resources, and the bravery to build something better.