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
| Part | What It Does | Approx. Price |
|---|---|---|
| Raspberry Pi 5 (8GB) | The brain of the router | ~£80 |
| ZDE ZP596 HAT + ZC506 case | Adds a second wired Ethernet port and a slot for the Wi-Fi card, all in an aluminium case | ~£45 |
| Intel AX210 Wi-Fi/Bluetooth card | Provides fast Wi-Fi 6E to create your wireless network | ~£15 |
| 2TB 2.5" SATA SSD | Stores the operating system and all your offline content | ~£80–100 |
| USB-to-SATA enclosure | Connects the SSD to the Pi via USB | ~£10–15 |
| 30W USB-C PD power bank | Powers 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 setup | You probably have one |
For the home coordination server:
| Part | What It Does | Approx. 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 supply | Powers the Pi 3 | ~£5–10 |
| Ethernet cable | Connects 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
| Part | What It Does | Approx. Price |
|---|---|---|
| Quectel RM502Q-AE 5G modem | Adds 5G/4G mobile data as a backup internet connection | ~£80–120 |
| EXVIST 5GM2A USB adapter board | Connects the modem to the Pi via USB and provides the power it needs | ~£20–30 |
| SIM card with data plan | Provides mobile data — any standard SIM works | Varies |
Or for a simpler (4G-only) option:
| Part | What It Does | Approx. Price |
|---|---|---|
| Huawei E3372 4G USB stick | Plug-and-play 4G modem, much simpler to set up | ~£30–40 |
| SIM card with data plan | Provides mobile data | Varies |
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
- Download and install the Raspberry Pi Imager on your computer from raspberrypi.com/software
- Plug the SSD (in its enclosure) into your computer via USB
- In the Imager:
- Device: Raspberry Pi 5
- Operating System: Raspberry Pi OS Lite (64-bit) — under “Raspberry Pi OS (other)”
- Storage: Select your SSD
- 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
- Hostname:
- 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:
- Write Raspberry Pi OS to a microSD card using the same Imager process above
- Put the microSD card in the Pi, connect a monitor and keyboard, and power it on
- Once it boots, open a terminal and run:
sudo raspi-config - Go to Advanced Options → Boot Order → USB Boot
- Select USB Boot and confirm
- Shut down the Pi:
sudo shutdown -h now - Remove the microSD card
- 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:
- 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
- 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)
- Stack the ZP596 HAT onto the Pi 5’s GPIO header and connect the flat PCIe ribbon cable
- 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:
- Insert your SIM card into the modem module
- Slot the modem into the EXVIST adapter board
- Connect the adapter board to a USB port on the Pi
- Connect the external antennas if your adapter has antenna connectors
If you’re using the simpler Huawei E3372:
- Insert your SIM card
- 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:
| Port | Name | Role |
|---|---|---|
| Built-in Ethernet | eth0 | Internet in — plug this into a hotel/Airbnb wired connection |
| 2.5G port on HAT | eth1 | Wired devices out — plug your laptop in here if you want a wired connection |
| AX210 Wi-Fi | wlan0 | Wi-Fi hotspot — your devices connect to this wirelessly |
| USB modem | wwan0 | Backup 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/htmllocation - Setting the default Wi-Fi country to your location (e.g.,
GBfor UK) - Installing the ad-blocking feature
When it finishes, reboot:
sudo reboot
Configure Your Wi-Fi Network
- 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 - Log in with the default credentials:
- Username:
admin - Password:
secret
- Username:
- Change the admin password immediately — go to System → Authentication
- 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
- Interface:
- Go to Hotspot → Advanced and set:
- Wi-Fi mode:
ac(oraxif your AX210 firmware supports it) - Channel: Pick a channel —
36or44are usually good choices for the 5GHz band
- Wi-Fi mode:
- 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
- Open the Pi-hole admin panel at
http://10.0.0.1/admin - Go to Settings → DHCP and:
- Enable DHCP server
- Range:
10.0.0.50to10.0.0.150 - Router (gateway):
10.0.0.1
- 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
- Go to the RaspAP panel at
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
- 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
- Hostname:
- Insert the microSD card into the Pi 3, plug in Ethernet to your ISP router, and power it on
- SSH in from your computer:
ssh admin@headscale.local - 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):
- Go to duckdns.org and log in with a GitHub, Google, or other account
- Create a subdomain (e.g.,
mytravelrouter) — this gives youmytravelrouter.duckdns.org - 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 Port | Internal IP | Internal Port | Protocol | Purpose |
|---|---|---|---|---|
| 443 | 192.168.1.200 (your Pi 3) | 8080 | TCP | Headscale coordination |
| 3478 | 192.168.1.200 (your Pi 3) | 3478 | UDP | Tailscale 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:
| Content | Size | Filename to look for |
|---|---|---|
| Wikipedia (English, with pictures) | ~109 GB | wikipedia_en_all_maxi_*.zim |
| Wikipedia (English, no pictures) | ~12 GB | wikipedia_en_all_nopic_*.zim |
| WikiMed (medical encyclopaedia) | ~1 GB | wikipedia_en_medicine_*.zim |
| Stack Overflow | ~28 GB | stackoverflow.com_en_all_*.zim |
| Project Gutenberg (70,000+ books) | ~75 GB | gutenberg_en_all_*.zim |
| iFixit repair guides | ~3 GB | ifixit_en_all_*.zim |
| Wiktionary (dictionary) | ~7 GB | wiktionary_en_all_maxi_*.zim |
| TED Talks | ~20 GB | ted_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
- With an Ethernet cable plugged into
eth0, confirm you have internet - Unplug the cable
- Wait about 30 seconds
- Check if you still have internet (now through mobile data)
- Plug the cable back in
- 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:
| Content | Space 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:
| Task | How |
|---|---|
| Connect to Wi-Fi | Join the network you named (e.g., TravelRouter) |
| Browse offline content | Open http://10.0.0.1 in any browser |
| Manage the router | Open http://10.0.0.1 → Router Settings |
| Check ad-blocking stats | Open http://10.0.0.1/admin |
| SSH into the Pi 5 | ssh 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 mesh | tailscale status |
| Run a manual backup | sudo /usr/local/bin/backup-config.sh |
| Run a manual content sync | sudo /usr/local/bin/sync-to-pi3.sh (home network only) |
| Restart all services | cd ~/services && docker compose restart |
| Check what’s running | docker ps |
| Update services | cd ~/services && docker compose pull && docker compose up -d |
| Safely shut down | sudo 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:
| Category | Examples | Can you get it back? |
|---|---|---|
| Configuration (small, irreplaceable) | Tailscale identity, Wi-Fi passwords, firewall rules, Docker compose file, Pi-hole settings, failover script | Only if you backed it up |
| Content (large, replaceable but tedious) | Wikipedia ZIM files, maps, music, films, e-books | Can 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:
| Backup | Where | What | Protects Against |
|---|---|---|---|
| Local — microSD card inside the Pi 5 | Travels with the router | Configuration only (a few MB) | SSD failure |
| Remote — external drive on the Pi 3 at home | Stays at home | Full mirror of the entire SSD — config, content, Docker volumes, everything | Theft, 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 volumes — Jellyfin’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 withdocker compose pull)- Docker container logs — regenerated when containers restart
- Swap files,
.cachedirectories — 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:
| Location | Contains | Survives |
|---|---|---|
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 config | Pi 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, everything | Pi 5 stolen, SSD failure |
Pi 3 external drive (/mnt/backup/pi3-headscale/) | Last 10 days of Headscale config | Pi 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.
- Get a new SSD and write Raspberry Pi OS Lite to it (Part 1 of this guide)
- Boot the Pi and do the basic setup (connect to your home network, enable SSH)
- 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 - 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 - 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.
- Get a new SSD and write Raspberry Pi OS Lite to it
- 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 / - 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.
- New hardware — same parts list from the top of this guide
- Set up the new Pi 5 and SSD — write Raspberry Pi OS Lite, boot, enable SSH (Parts 1–2)
- 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 - Reinstall the software (same as Option A, step 4)
- 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:
- Flash a new microSD with Raspberry Pi OS Lite
- 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 - Restart the cron service so the restored DuckDNS cron job takes effect:
sudo systemctl restart cron - 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.
Encrypt the Backups (Optional but Recommended)
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

