Audit before you touch anything
The plan assumed Traefik and Gitea were running on the VPS. They weren’t — and finding that out read-only, before changing anything, shaped the whole rebuild.
The read-only inventory
Section titled “The read-only inventory”None of these need root or change anything:
hostname; uname -m # which machine, which CPUdocker ps -a; docker network ls # containers and networksdocker volume ls; docker image ls # what data and images remainss -ltn # what is listening, and wheresystemctl list-units --type=service --state=runningls /opt /srv /etc/*infra* 2>/dev/null # where configs might livedocker system df -v # volume sizesWhat it showed here:
- Zero containers, no custom networks — but 13 volumes (Traefik certs, a
Gitea with registry and runner, several apps) and 23 images. A previous Docker
Swarm stack had been removed by hand (
docker service rm,prune,swarm leave), leaving its data. - Nothing listening publicly.
sshdon127.0.0.1only (reached through a Cloudflare Tunnel), so every hostname returned Cloudflare’s 521 (“origin down”). - The CPU is
x86_64, while the laptop building images wasarm64— an image built locally would not run there (Going live). - A newer infrastructure repo (
/opt/vps-infra) had been cloned but never started: no.env, emptysecrets/.
The rule: leave the old system alone
Section titled “The rule: leave the old system alone”The old images, volumes and stack files are a working reference for how the previous setup was configured. So until the new system is live:
- no
docker system prune,image prune,volume prune— nor wrapper targets that call them; - old volumes are never mounted, even read-write by a new service;
- new services get fresh volumes, and the names are checked not to collide.
They’re retired at the very end, by name, after backups work (Retiring).
Back it up anyway
Section titled “Back it up anyway”The volumes were the only copy of that data. Each one goes into a tarball via a throwaway container that mounts it read-only:
sudo mkdir -p /var/backups/pre-rebuild && sudo chmod 700 /var/backups/pre-rebuildfor v in <volume-1> <volume-2> …; do sudo docker run --rm -v "$v":/src:ro -v /var/backups/pre-rebuild:/out alpine:latest \ tar czf "/out/$v.tgz" -C /src . && echo "ok $v"doneThen prove every archive is complete — compressed stream intact, archive readable, and the same number of entries as the source volume:
for f in /b/*.tgz; do gzip -t "$f" && tar tzf "$f" >/dev/null && echo "intact $f ($(tar tzf "$f" | wc -l) entries)"; donedocker run --rm -v <volume>:/s:ro alpine:latest sh -c 'find /s | wc -l'