Skip to main content
ariefrse.my
HomeSkillsProjectsAI WorkWritingContact
Resume
All writing

Note · Infrastructure

How my site updates itself minutes after I save my work

A push-to-live loop with no SSH step, and the two footguns that each cost me a debugging session. Both failed invisibly from the outside.

July 6, 2026 · 5 min read

The deploy loop for my self-hosted portfolio has no SSH step in it. GitHub Actions builds a Docker image on every push to main and publishes it to a registry. On the server, Watchtower polls that registry every 60 seconds and restarts the container when a new image appears.

The server never checks out the repo and never runs a build. docker compose up on any machine is a full deploy target.

# docker-compose.yml
web:
  image: ghcr.io/ariefrsee/portfolio:latest
watchtower:
  image: containrrr/watchtower
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock

Push, CI, registry, live. Two footguns cost me a debugging session each, and they share a shape worth noticing.

Footgun 1: build-time env vars do not exist in CI

With a static export, NEXT_PUBLIC_* values are baked in at build time, not read at runtime. My local builds had them. The CI workflow did not pass them as build-args.

The result deployed green. Working site, working build, and a contact form that silently posted to nothing. Every visible signal said success, because every visible signal was about the build rather than the behaviour.

Footgun 2: Watchtower's Docker client is ancient

Watchtower's bundled client speaks Docker API 1.25. A current daemon rejects anything below 1.40. Every poll cycle errored before it ever reached the registry, so auto-deploy simply stopped happening.

time="2026-07-06T02:14:01Z" level=info msg="Watchtower polling registry"
time="2026-07-06T02:14:01Z" level=fatal msg="client version 1.25 is too old, minimum supported API is 1.40"

(repeats every 60s: container reports "healthy," site stays stale)

No crash. Container healthy. Deploys frozen. The fix is one environment variable:

# docker-compose.yml
watchtower:
  environment:
    - DOCKER_API_VERSION=1.44

The shape they share

Both failures ran the pipeline successfully and were invisible from the outside. Neither produced a red mark anywhere. In both cases the thing that was broken was downstream of everything that was being checked.

That pattern turned out to be general enough that I started collecting instances of it, and general enough that I eventually made CI verify the live site directly.

What I learned

After wiring any auto-deploy loop, verify the loop itself rather than its parts. Push a trivial visible change and watch it arrive.

And read the poller's logs once, deliberately, rather than never. A component that reports healthy while failing every cycle will not come and find you.

dockerwatchtowerdeploymentci

Share

XLinkedInWhatsAppThreads

Related

  • A green build is not a deploy: making CI check the live siteEvery stage of my pipeline vouched for itself and nobody vouched for the result. The first real run of the fix caught ten days of deploys that had gone nowhere.
  • Everything fails silentlyGreen is not evidence. It is the absence of evidence. A running gallery of production failures that passed every check that existed.

© 2026 Muhammad Arief Roseli

GitHubLinkedInSnapInvois