---
title: Updating the Website Repository on a VPS
description: A repeatable update flow for pulling the latest website code on a VPS and redeploying safely.
pubDate: '2026-03-04T12:44:00+01:00'
category: en/on-premises-private-cloud
tags:
  - podman
  - deployment
seriesParent: astro-ssr-with-podman
seriesOrder: 5
---

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.

```bash
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:

```bash
git stash push -m "vps-local-before-update"
```

## Pull the latest code

```bash
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

```bash
cd /opt/website
podman-compose -f compose.yml down
podman-compose -f compose.yml up --build -d
```

or, as a one-shot:

```bash
podman-compose -f compose.yml up --build -d --force-recreate
```

If the container's state is wedged, check first and remove the offender:

```bash
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:

```bash
# Option A: clean compose state
podman-compose -f compose.yml down
podman-compose -f compose.yml up --build -d
```

```bash
# 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:

```bash
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:

```bash
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:

```bash
#!/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:

```bash
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.
