Skip to content
brainNotFound

Server Setups/Operations

Zero-downtime deploys without Kubernetes

Symlink releases, a socket-activated restart and a rollback that takes one command and two seconds.

advanced30 min

// read first

On this page
  1. The layout
  2. The deploy script
  3. Why not blue/green?
  4. Environment

The version of this most people reach for is a container orchestrator. For a single service on a single host, the orchestrator is more moving parts than the thing it deploys.

What follows is the Capistrano-shaped layout — timestamped releases, an atomic symlink swap, a current pointer — with a modern restart strategy bolted on.

The layout

/home/deploy/app
/home/deploy/app
├── current -> releases/20260701T090000   # atomic symlink
├── releases/
│   ├── 20260628T140000/
│   ├── 20260630T101500/
│   └── 20260701T090000/
├── shared/
│   ├── .env                              # symlinked into each release
│   └── uploads/                          # survives deploys
└── repo/                                 # bare git mirror

Everything mutable lives in shared and is symlinked into the release. A release directory is therefore disposable, which is what makes rollback trivial.

The deploy script

deploy.sh
#!/usr/bin/env bash
set -Eeuo pipefail

APP=/home/deploy/app
REL="$APP/releases/$(date -u +%Y%m%dT%H%M%S)"

git --git-dir="$APP/repo" fetch --prune origin main
mkdir -p "$REL"
git --git-dir="$APP/repo" archive origin/main | tar -x -C "$REL"

ln -sfn "$APP/shared/.env" "$REL/.env"
ln -sfn "$APP/shared/uploads" "$REL/uploads"

cd "$REL"
npm ci --omit=dev
npm run build

# Atomic: ln -sfn on the same filesystem is a rename(2).
ln -sfn "$REL" "$APP/current.new"
mv -Tf "$APP/current.new" "$APP/current"

sudo systemctl reload-or-restart app
"$APP/bin/healthcheck" || { echo "health check failed"; exit 1; }

ls -1dt "$APP"/releases/* | tail -n +6 | xargs -r rm -rf

Why not blue/green?

Because with a symlink swap and a two-second restart, the outage window is smaller than the health-check interval of most load balancers. Blue/green earns its complexity when you have more than one host to coordinate; below that it is ceremony.

The right amount of infrastructure is the least you can operate confidently at 3am, half-awake, from a phone.

Environment

shared/.env
VariableDescriptionRequired
APP_RELEASE20260701T090000Injected by the deploy script. Surfaced in error reports to identify the build.Optional
HEALTHCHECK_PATH/healthzEndpoint the deploy script polls. Must not touch the database.Required
shared/.env

// related

From the rest of the site.