From git push to live: the deployment pipeline in detail

Starting point

After every blog post I no longer wanted to touch the VPS by hand. A git push should be enough to update the site — no SSH, no manually run docker compose.

Decision

Instead of copying the built dist/ folder to the server via rsync, I build a Docker image tagged with the commit SHA. An image is immutable and turns a rollback into a tag change rather than a new build — and matches the pattern the upcoming apps need anyway.

Implementation

Two GitHub Actions jobs: build builds the image (multi-stage, Node → nginx:alpine) and pushes it to ghcr.io tagged latest and with the commit SHA. deploy, gated behind the DEPLOY_ENABLED variable, connects over its own SSH deploy key and runs docker compose pull && up -d --force-recreate on the VPS. A validation script runs as a prebuild hook right inside the image build and catches missing translations.

Takeaway

A green run isn’t automatically a finished pipeline — moving references like ubuntu-latest or unpinned action versions age silently in the background. The reasoning behind each decision lives in the longer write-up, The pipeline, under the hood.