SD Card Distribution
This guide explains how to prepare and ship a Raspberry Pi SD card image with the Digitization Toolkit pre-installed, so the recipient can plug it in and access the application from any device on the same network — without any manual IP configuration.
How It Works
The Raspberry Pi broadcasts its hostname over the local network using mDNS (via the Avahi daemon, pre-installed on Raspberry Pi OS). This means any device on the same network can reach the Pi at http://<hostname>.local without knowing its IP address.
The application is configured to use this hostname instead of a hardcoded IP, so the image works on any network regardless of what IP the router assigns.
Preparing the Master Image
1. Set a meaningful hostname
Choose a hostname that identifies the device. The current convention is digitool:
sudo hostnamectl set-hostname digitoolVerify:
hostname
# digitoolThe Pi will be reachable at http://digitool.local on the destination network.
2. Confirm Avahi is running
Avahi (mDNS) is pre-installed on Raspberry Pi OS. Confirm it is active:
systemctl is-enabled avahi-daemon
systemctl is-active avahi-daemonBoth should return enabled / active. If not:
sudo systemctl enable --now avahi-daemon3. Update .env to use the hostname
In /home/pi/dtk/.env, set HOST_IP to the .local hostname:
HOST_IP=digitool.local
CORS_ORIGINS=["http://digitool.local","http://localhost","http://localhost:5173","http://localhost:3000"]In the production stack, everything goes through the Nginx reverse proxy on port 80 (docker-compose.pi.yml), so the origin users actually open is http://digitool.local with no port. PUBLIC_API_BASE resolves automatically from HOST_IP and is routed through Nginx’s /api/ location, not straight to the backend port:
PUBLIC_API_BASE=http://${HOST_IP}/api4. Apply the new environment and verify
PUBLIC_API_BASE is read at runtime by the SvelteKit server (via $env/dynamic/public), not compiled into the frontend image — so recreating the containers with the updated .env is enough; no image rebuild is required for .env changes:
cd /home/pi/dtk
docker compose -f docker-compose.yml -f docker-compose.pi.yml up -d --force-recreate(Only add --build if the frontend source itself changed, not for .env edits.)
Open http://digitool.local from another device on the same network to confirm it loads.
5. Image the SD card
Once the system is working correctly, shut down the Pi cleanly and image the SD card from another machine:
macOS / Linux:
# Identify the SD card device (e.g. /dev/disk4 or /dev/sdb)
diskutil list # macOS
lsblk # Linux
# Create compressed image
sudo dd if=/dev/sdX bs=4M status=progress | gzip > dtk-$(date +%Y%m%d).img.gzWindows: Use Raspberry Pi Imager or Win32DiskImager to read the card to a .img file.
Tools like PiShrink can reduce the image size significantly before compression by trimming unused filesystem space.
SECRET_KEY and DATABASE_PASSWORD are regenerated automatically on each physical unit by dtk-secrets.service (installed by install-service.sh). It records the Pi’s hardware serial in /var/lib/dtk/secrets-provenance; when a flashed clone boots on a different Pi, the serial mismatch triggers fresh secrets before the backend starts — including rotating the Postgres password inside the cloned data directory. You do not need to scrub secrets from the master image, and test-booting the master before imaging is fine.
Flashing and Delivering the Image
Flash the image onto a new SD card:
# Linux / macOS
gzip -dc dtk-YYYYMMDD.img.gz | sudo dd of=/dev/sdX bs=4M status=progressOr use Raspberry Pi Imager → Use custom image → select the .img.gz file.
What the Recipient Needs to Do
Nothing, in most cases. They:
- Insert the SD card and power on the Pi.
- Connect the Pi to their local network (Ethernet or Wi-Fi configured before imaging).
- Open
http://digitool.localfrom any browser on the same network.
If the destination uses Wi-Fi (not Ethernet), configure the network credentials on the SD card before shipping.
Raspberry Pi OS Bookworm replaced dhcpcd + wpa_supplicant with NetworkManager, so dropping a wpa_supplicant.conf on the /boot partition no longer works — Bookworm ignores that file.
- Preferred: use Raspberry Pi Imager’s Advanced options (gear icon, or
Ctrl+Shift+X) when flashing to preconfigure the Wi-Fi SSID and password before the card is ever booted. - Post-flash: configure Wi-Fi via NetworkManager, e.g.
sudo nmcli device wifi connect "<SSID>" password "<password>", or by dropping a NetworkManager keyfile connection profile under/etc/NetworkManager/system-connections/.
Troubleshooting
digitool.local doesn’t resolve
- Windows requires Bonjour (installed automatically with iTunes or iCloud). Windows 10 (1803+) supports mDNS natively.
- Try pinging:
ping digitool.local - As a fallback, find the IP via the router’s DHCP client list and use it directly.
CORS errors in the browser
Under the production Nginx setup, the browser talks to a single origin (http://digitool.local, port 80) for both the frontend and /api/ — this same-origin reverse-proxy model means this class of CORS error should not occur in normal production use. If you still see one, the frontend origin most likely does not match CORS_ORIGINS in .env (for example after renaming the hostname). Update that list and restart the backend in production mode:
cd /home/pi/dtk/backend && pixi run startTwo Pis on the same network
mDNS hostnames must be unique per network. Change the hostname on one of them:
sudo hostnamectl set-hostname digitool-2And update HOST_IP and CORS_ORIGINS in .env accordingly, then rebuild the frontend container.