veta-auto-pull (continuous deploy)
veta-auto-pull is the continuous deployment mechanism for the
production server. Every 5 minutes a systemd timer fires a script that:
- Polls
origin/mainviagit ls-remote(no full clone) - Compares the remote SHA against
state/last-deployed-sha - If different, clones main shallow and self-installs the latest
homelab-deploy.shover/opt/stacks/veta/deploy.sh - Runs
deploy.shwhich rsyncs compose files and runsdocker compose up -dwith healthcheck gating - On success, writes the new SHA to
state/last-deployed-sha - On failure, leaves the file unchanged so the next tick retries
This replaced Watchtower in 2026-05 (Watchtower had a recreate-name collision bug that left the platform half-deployed indefinitely).
Why not GitHub Actions?
Section titled “Why not GitHub Actions?”.github/workflows/ci.yml originally had a deploy job that
SSHed from a GH runner to the server’s LAN address. GitHub runners are on public
IPs; the server is on a private LAN. The ssh-keyscan step always
failed.
Inverting the direction so the server polls GitHub sidesteps the network problem entirely without a tunnel, a Tailscale, or a self-hosted runner.
The 5-minute cadence is the sweet spot:
- Fast enough that operators do not notice the lag (~3.5 min mean)
- Slow enough to avoid hammering GitHub’s API
- Aligns with the 6-minute wait in the post-deploy CI probe
Install (one-time, on the server)
Section titled “Install (one-time, on the server)”# 1. Copy the auto-pull script to the stack dirsudo install -m 0755 \ /path/to/repo/scripts/homelab-auto-pull.sh \ /opt/stacks/veta/auto-pull.shsudo chown miles:miles /opt/stacks/veta/auto-pull.sh
# 2. Install systemd unitssudo install -m 0644 \ /path/to/repo/scripts/homelab-systemd/veta-auto-pull.service \ /etc/systemd/system/sudo install -m 0644 \ /path/to/repo/scripts/homelab-systemd/veta-auto-pull.timer \ /etc/systemd/system/
# 3. Enable + startsudo systemctl daemon-reloadsudo systemctl enable --now veta-auto-pull.timerState files
Section titled “State files”| Path | Written by | Meaning |
|---|---|---|
state/last-deployed-sha | auto-pull.sh | Last main SHA we successfully ran deploy.sh for |
state/auto-pull.lock | auto-pull.sh | flock for single-instance guard |
.good-sha | deploy.sh | Last SHA the gateway reported as its baked-in version after a successful deploy |
.good-sha and last-deployed-sha may legitimately diverge by one CI
cycle. They answer different questions and one is not a corrupted copy
of the other. See
the SHA semantics note below.
Daily use
Section titled “Daily use”# When does it next run?systemctl list-timers veta-auto-pull.timer
# Recent activity (deploys, retries, errors)journalctl -u veta-auto-pull.service -n 50 --no-pager
# Force a check now (e.g. you just merged something hot)sudo systemctl start veta-auto-pull.service
# Pause auto-deploys (e.g. during a manual debug session)sudo systemctl stop veta-auto-pull.timer
# Resumesudo systemctl start veta-auto-pull.timerSHA semantics
Section titled “SHA semantics”The two SHA files answer different questions:
state/last-deployed-sha: “what main SHA did I last try to deploy?”.good-sha: “what SHA did the gateway report as its baked-in version after the deploy succeeded?”
These may diverge by one CI cycle: if main moves twice in 5 min
(dependabot plus a feature merge), CI rebuilds the gateway image twice.
By the time the second auto-pull tick fires, the gateway image for the
newer SHA might not be ready yet on GHCR. The pull picks up the
older image, the gateway reports version = X, and .good-sha = X
while last-deployed-sha = Y.
This is expected, not a drift bug.
Source
Section titled “Source”scripts/homelab-auto-pull.shscripts/homelab-deploy.shscripts/homelab-systemd/veta-auto-pull.servicescripts/homelab-systemd/veta-auto-pull.timer
Related
Section titled “Related”- Edge architecture: how public traffic reaches what auto-pull deploys
- CI/CD pipeline: what happens before the server pulls the image
- veta-tunnel: the secure tunnel that exposes the server publicly
- veta-host-prune: weekly cleanup of dangling images
- Synthetic probe: closes the loop with a post-deploy check