Updating the Website Repository on a VPS

Category: On-Premises & Private Cloud

Tags: podman, deployment


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-compose managing the container, Caddy in front as reverse proxy.
  • Branch model: main is 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-only on every pull. Fast-forward or fail; no merge commits on a production server.
  • Keep the working tree clean. If git status on the VPS is ever dirty, something is wrong upstream of this flow.
  • Verify from outside. curl against 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.