Updating the Website Repository on a VPS
A repeatable update flow for pulling the latest website code on a VPS and redeploying safely.
Category: On-Premises & Private Cloud
Once the site was running on a VPS, I wanted updates to be boring — the same six commands every time, in the same order, with no surprises at step four. This post documents the flow I settled on: pull, rebuild, verify, and a rollback path for the days it does go sideways.
The setup
- VPS: Debian, project checked out at
/opt/website. - Runtime:
podman-composemanaging the container, Caddy in front as reverse proxy. - Branch model:
mainis production, deploys are pull-based on the server.
Inspect state first
Before pulling anything, I want to know what the working tree looks like — a dirty repo on the server is a symptom of something nobody should have done, and I’d rather catch it than paper over it.
cd /opt/website
git status
git branch --show-current
git remote -v
If there are local changes (there shouldn’t be, but), stash them before the pull:
git stash push -m "vps-local-before-update"
Pull the latest code
cd /opt/website
git fetch --all --prune
git pull --ff-only origin main
--ff-only is the important flag — it prevents an accidental merge commit on the server if someone ever committed directly to the VPS’s copy. Fast-forward or fail; no silent merges.
Rebuild and restart
cd /opt/website
podman-compose -f compose.yml down
podman-compose -f compose.yml up --build -d
or, as a one-shot:
podman-compose -f compose.yml up --build -d --force-recreate
If the container’s state is wedged, check first and remove the offender:
podman ps -a --filter name=www.adrian-altner.de
podman rm -f www.adrian-altner.de
Problem: the container name is already in use. This is the single most common hiccup — compose.yml pins container_name: website, and a previous run left a stopped container behind. Two recovery paths depending on how deep the mess is:
# Option A: clean compose state
podman-compose -f compose.yml down
podman-compose -f compose.yml up --build -d
# Option B: remove conflicting container explicitly
podman rm -f website
podman-compose -f compose.yml up --build -d
Option A is the preferred move; Option B is for when down fails to clean up properly.
Verify
Four checks, local to public:
podman ps
podman logs --tail=100 website
curl -I http://127.0.0.1:4321
curl -I https://adrian-altner.de
If Caddy is active, the public HTTPS check is the final signal — anything earlier only tells you the container is alive, not that the reverse proxy still routes to it.
Fast rollback
When a deploy breaks production, the priority is restoring service, not diagnosing. Check the history, check out the last known good commit, rebuild:
cd /opt/website
git log --oneline -n 5
git checkout <previous-commit>
podman-compose -f compose.yml up --build -d
After recovery, fix the bug in the repo on your laptop and deploy forward again from main — the server should not sit on a detached HEAD for long.
One-command deploy script
The above flow compressed into something I can run without thinking:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/website
git fetch --all --prune
git pull --ff-only origin main
if ! podman-compose -f compose.yml up --build -d; then
podman rm -f website || true
podman-compose -f compose.yml up --build -d
fi
curl -fsS -I http://127.0.0.1:4321 >/dev/null
echo "Deploy successful"
Save as deploy.sh, make executable, run:
chmod +x deploy.sh
./deploy.sh
The if ! ... || podman rm -f fallback is the same pattern I use from CI — one retry that covers the stale-container case without aborting the script.
What to take away
--ff-onlyon every pull. Fast-forward or fail; no merge commits on a production server.- Keep the working tree clean. If
git statuson the VPS is ever dirty, something is wrong upstream of this flow. - Verify from outside.
curlagainst the public URL is the only check that proves Caddy still routes correctly. - Keep a rollback rehearsed.
git log+git checkout <sha>+ rebuild is the shortest path back to a working site. - A predictable update flow is more important than a complex one. Boring is a feature.