Build and test a three-node Raspberry Pi Docker Swarm with a single set of scripts. The repo installs Docker, bootstraps the swarm, and deploys a demo stack that proves overlay-network traffic between nodes. Guardrails such as idempotent installs, token validation, and preflight network checks keep the process repeatable on fresh or recycled Pis.
Validated on Raspberry Pi OS 64-bit (Bookworm). Docker and the demo images are multi-arch, so the flow works on Pi 3/4/5.
- Three Raspberry Pi boards with a 64-bit OS, SSH enabled, and static/reserved
IP addresses (example: manager
192.168.88.7, workers192.168.88.5and192.168.88.8) - A shared LAN plus consistent hostnames (examples:
pi-manager,pi-worker1,pi-worker2) sudoaccess on every node- (Optional) DNS or mDNS so you can visit
http://pi-manager:8080
Pick the one-liner flow for each node or follow the step-by-step commands below.
- Manager:
sudo bash scripts/bootstrap-node.sh manager 192.168.88.7 - Worker:
sudo bash scripts/bootstrap-node.sh worker 192.168.88.7 SWMTKN-1-...
The helper chains the existing scripts: managers install Docker, initialize the
swarm, and deploy the demo stack; workers install Docker and join using the
provided token (prompted if omitted). If the manager adds cgroup flags to
/boot/cmdline.txt, it offers a reboot before continuing.
Each step is a single command. Follow them in order and run the commands exactly as shown on the indicated nodes.
- Clone this repo onto every Pi (or copy
scripts/andstack/). - Ensure
sudoworks without a password prompt if you plan to run unattended.
Run on every Pi (manager + workers):
sudo bash scripts/install-docker.shReboot once so the added cgroup flags in /boot/cmdline.txt take effect. The
script is safe to rerun whenever you add or rebuild a node.
Run only on pi-manager. Swap in your advertise IP if different (example
below uses 192.168.88.7):
sudo bash scripts/init-swarm.sh 192.168.88.7The command prints fresh manager and worker join tokens plus the
advertise address. It also builds and deploys the web control module so the
dashboard is ready at http://<manager-ip>:8081 immediately. If the node
already belongs to a swarm, the script exits with the current membership details
instead of reinitializing.
Run on pi-worker1 and pi-worker2 using the worker token from step 2:
sudo bash scripts/join-swarm.sh 192.168.88.7 SWMTKN-1-...If a worker is already in a swarm, leave first with docker swarm leave --force
and rerun the command. Confirm membership from the manager:
docker node lsRun on the manager to create the overlay network and demo services:
sudo bash scripts/deploy-demo.shThe compose file (stack/demo.yml) launches:
- hello-app –
traefik/whoamiwith 2 replicas (one per worker) on port 8080. - redis-bus –
redis:7-alpinepinned to the manager with a named volume for durability. - echo-worker – two curl-based workers that continually hit
hello-appacross the overlay network, logging responder hostname and latency from the worker nodes to avoid loading the manager. - swarm-dashboard – Flask UI on port 8081 (manager-only) that streams swarm health, replica counts, and Redis reachability from the Docker API. The new Control Center panel surfaces join commands, one-click token rotation, service scaling, and node drain/activation switches so you can grow the cluster without reaching for the CLI.
App workloads intentionally land on workers so the manager stays available for
control-plane tasks. When you deploy your own services, add a placement
constraint such as node.role == worker to keep them off the manager, or adjust
the replica count if you add more nodes to the cluster.
- Hit the published service:
curl http://192.168.88.7:8080(orhttp://pi-manager.local:8080). TheHostnameshould vary between nodes. - Open the dashboard at
http://192.168.88.7:8081for live metrics and overlay checks. - Watch the worker logs from inside the cluster:
docker service logs -f swg-demo_echo-worker- Confirm service placement:
docker service ps swg-demo_hello-app- Confirm Redis health within the overlay:
docker run --rm --network cluster_net redis:7-alpine redis-cli -h redis-bus PINGdocker stack rm swg-demoThe overlay network cluster_net is created once and reused; remove it with
docker network rm cluster_net if you want a full cleanup.
Use stack/textures.yml to ship a Redis-backed texture pipeline that shares a
single volume across ingestion, upscaling, and LLM metadata analysis workers.
Placement constraints keep replicas on workers, with optional GPU labels for the
Real-ESRGAN nodes.
The stack expects a shared export at /srv/textures mounted on every swarm
node. The example below uses an NFS export from the manager; adjust the server
address if you use a NAS or other host.
-
On the chosen NFS server (for example the swarm manager), create the export and ensure it is writable by the service users:
sudo mkdir -p /srv/textures sudo chown -R 1000:1000 /srv/textures # Add to /etc/exports (example): # /srv/textures 192.168.88.0/24(rw,sync,no_subtree_check) sudo exportfs -rav
-
Create the swarm volume on every node so the external volume can be attached when the stack is deployed. Replace
pi-managerwith your NFS server's IP/hostname if different:docker volume create \ --driver local \ --opt type=nfs \ --opt o=addr=pi-manager,rw,nfsvers=4 \ --opt device=:/srv/textures \ textures-share -
Verify the mount works on the manager and each worker before deploying the stack:
docker run --rm -v textures-share:/srv/textures alpine sh -c "touch /srv/textures/.probe && ls -l /srv/textures/.probe"Confirm the probe file appears on all nodes (remove it afterwards if desired).
-
(Optional) Label GPU-capable nodes so the Real-ESRGAN workers land where hardware acceleration is available:
docker node update --label-add gpu=true <node-name>
-
If you are deploying the texture stack without first running the demo stack, create the shared overlay network in advance (idempotent and safe to rerun):
docker network create --driver overlay --attachable --opt encrypted=true cluster_net
-
Deploy the stack with the helper script, which auto-scales GPU replicas to the number of nodes labeled
gpu=true(or zero if none) and reuses the existingcluster_netoverlay network created by the demo scripts:sudo scripts/deploy-textures.sh stack/textures.yml swg-textures
The compose file includes:
- redis-bus – shared message queue on the manager for simple channel
orchestration (
texture:ingest,texture:upscaled,texture:metadata). - texture-ingestion – publishes new work from
/srv/textures/incominginto Redis so the upscalers can pick it up. - esrgan-upscaler-gpu – Real-ESRGAN workers pinned to GPU-labeled
workers (replicated across nodes with
max_replicas_per_node: 1). The helper script setsdeploy.replicasto the number of GPU nodes discovered viadocker node ls --filter node.label=gpu=trueso the service scales with available hardware. - esrgan-upscaler-cpu – CPU fallback upscalers for non-GPU workers. When
no GPU nodes are present, the helper script sets GPU replicas to zero and
these workers continue handling requests. Listens on
INPUT_CHANNELforscanevents, pulls textures from/srv/textures/incoming, writes CPU-safe upscales into/srv/textures/output, and emits status/results onOUTPUT_CHANNEL(JSON events for online status, completed scans, successes, and errors). - metadata-analyzer – LLM-driven metadata annotator that reads upscales, generates captions/tags through a configurable LLM backend, and publishes structured metadata on its own Redis channel while writing sidecar JSON next to each texture.
Configure the analyzer with
LLM_BACKEND(for exampleopenai,local, or the defaultechomode),LLM_MODEL,LLM_ENDPOINT, andLLM_API_KEYas needed. Metadata is persisted alongside each texture with the suffix.metadata.jsonand mirrored to thetexture:metadatachannel together with latency metrics for observability.All services mount the shared
textures-sharevolume at/srv/texturesso inputs/outputs stay consistent across nodes. The stack reuses the pre-existingcluster_netoverlay so Redis and the workers can reach each other without extra configuration. - redis-bus – shared message queue on the manager for simple channel
orchestration (
Use scripts/export-and-sync.sh to watch the shared output volume and push new
textures to a dedicated Git repository (for example, an HDtextureDDS repo).
-
What it does:
- Watches
WATCH_DIR(default:/srv/textures/output) for new or updated files and stages only texture artifacts that matchFILE_PATTERNS(default:*.dds *.png). - Syncs staged files into
EXPORT_DIR(default:/srv/textures/export) usingrsync, commits them, and pushes toGIT_REMOTE_URLonGIT_BRANCH. - Optionally pulls
polsommer/HDtextureDDSand runs its preprocess/upscaler entrypoints before each sync so DDS assets are converted and enhanced by your configured LLM pipeline. - Supports SSH deploy keys via
GIT_SSH_KEY_PATHor HTTPS tokens embedded inGIT_REMOTE_URL(e.g.,https://<token>@github.com/org/HDtextureDDS.git). - Logs to stdout and, if
LOG_FILEis set, appends to the specified file for operators to monitor.
- Watches
-
Dependencies:
-
gitfor cloning, committing, and pushing updates. -
rsyncfor mirroring rendered textures into the export workspace. -
Optional:
inotifywait(frominotify-tools) for event-driven file watching; otherwise the script falls back to interval polling. -
Install the Raspberry Pi OS dependencies with:
sudo apt-get install -y git rsync inotify-tools
inotify-toolsis optional, so skip it if unavailable.
-
-
Configuration: by default, the script loads environment variables from a
.envfile next to the script (ENV_FILEdefaults toscripts/.env). You can overrideENV_FILEor set env vars in the service unit to avoid hardcoding secrets:WATCH_DIR=/srv/textures/output EXPORT_DIR=/srv/textures/export FILE_PATTERNS="*.dds *.png" GIT_REMOTE_URL=git@github.com:org/HDtextureDDS.git GIT_BRANCH=main GIT_SSH_KEY_PATH=/home/pi/.ssh/deploy_key GIT_USER_NAME=Texture Sync Bot GIT_USER_EMAIL=bot@example.com LOG_FILE=/var/log/texture-export.log # HDtextureDDS automation HD_DDS_ENABLED=1 HD_DDS_AUTO_PROCESS=1 HD_DDS_UPSCALE_SCRIPT=/srv/SWG-RPI-Cluster/scripts/HDtextureDDS/tools/upscale_with_llm.sh HD_DDS_SOURCE_DIR=/srv/textures/output HD_DDS_OUTPUT_DIR=/srv/textures/output # Optional: pass additional arguments to the upscaler entrypoint HD_DDS_UPSCALE_ARGS="--model my-llm --strength 0.8" # Optional: override where staged files are archived if pushes can't reach GitHub FALLBACK_ARCHIVE_DIR=/srv/textures/export/manual_review
-
Syncing to the
polsommer/HDtextureDDSrepo: setGIT_REMOTE_URL=https://github.com/polsommer/HDtextureDDS.git(or the SSH equivalent) andGIT_BRANCH=mainin your.env. On the first run, the script clones the existing history intoEXPORT_DIRbefore watching for new assets, preserving the repo’s commit lineage while automatically pushing subsequent renders from the cluster. -
Running manually:
bash scripts/export-and-sync.sh
-
Scheduling options:
-
systemd service + timer (recommended): bundled units live in
systemd/texture-sync.serviceandsystemd/texture-sync.timer, referencing/srv/textures/.envplus the repo’sscripts/export-and-sync.shpath. Install and enable them with one command:sudo bash scripts/install-texture-sync-service.sh
The helper copies the units into
/etc/systemd/system, reloads the daemon, and startstexture-sync.timerimmediately. -
Cron: run a periodic sync if
inotifywaitisn’t available:*/5 * * * * /usr/bin/env bash /srv/SWG-RPI-Cluster/scripts/export-and-sync.sh >> /var/log/texture-export.log 2>&1
-
Swarm service: deploy as a sidecar that mounts the shared volume and your SSH key/
.envsecret:docker service create \ --name texture-sync \ --mount type=bind,src=/srv/textures,dst=/srv/textures \ --secret source=texture-env,target=/srv/textures/.env,mode=0400 \ --mount type=bind,src=/home/pi/.ssh/id_ed25519,dst=/id_ed25519,ro \ --env GIT_SSH_KEY_PATH=/id_ed25519 \ --env ENV_FILE=/srv/textures/.env \ --restart-condition=any \ bash:5.2 bash -lc '/workspace/SWG-RPI-Cluster/scripts/export-and-sync.sh'
-
-
Failure handling and logging:
- Script exits on errors and writes tagged log lines; configure
LOG_FILEandsystemdjournal forwarding to alerting if needed. - Pushes are skipped (but not fatal) when
GIT_REMOTE_URLis unset so local staging can proceed. When GitHub upload isn’t available, staged files are copied to a timestamped archive inFALLBACK_ARCHIVE_DIR(defaults to<EXPORT_DIR>/manual_review) for manual inspection/upload. Restart=alwaysin systemd or--restart-condition=anyin swarm keeps the watcher alive after transient failures.
- Script exits on errors and writes tagged log lines; configure
The Flask-based UI can be deployed without the demo services if you only need a
live swarm view. See dashboard/README.md for a
step-by-step guide that covers building the image, wiring up cluster_net, and
deploying the service with a one-liner stack file.