Plenty of small teams deploy at midnight and hope. The late hour is not caution, it is a tell: the process is not safe, so it gets scheduled for when the fewest people will notice if it breaks. Fix the process and the timing stops mattering, and you can deploy at 11 a.m. with a colleague watching a graph. Below is the checklist we hand HOUSE603 clients and the mechanics under each line.
1. Build an immutable, versioned artifact
Tag every build with the commit SHA and never rebuild what you call the same release. If app-9f2a1c.tar.gz is the file that passed CI, that exact file is what goes to production and, if it comes to it, that exact file is what you roll back to. Rebuilding from source on the way out the door is how a green pipeline still ships a surprise. Everything else here rests on this one habit.
2. Make database migrations backward compatible
For a few seconds during every switch, the old code and the new code are both live and hitting the same database, so the schema has to keep both happy. The pattern that makes this safe is expand then contract. The first release adds the new columns or tables and nothing else. The code that reads and writes the new shape ships next, while still tolerating the old one. Only once nothing touches the old shape does a later release remove it. Burn this failure mode into your memory: never rename or drop a column in the same deploy that ships the code depending on that change, because that is a guaranteed few seconds of 500s.
3. Release beside the running version
Bring the new version up on a new port while the current one keeps serving every request. A reverse proxy turns the eventual switch into a one-line change. With nginx you point an upstream at whichever release should be live:
# /etc/nginx/conf.d/app.conf
upstream app {
server 127.0.0.1:8081; # flip to 8082 for the new release, then reload
}
server {
listen 80;
location / {
proxy_pass http://app;
proxy_next_upstream error timeout http_502 http_503;
}
}
4. Pass a health check before receiving traffic
The new version has to earn its traffic. A health endpoint that returns 200 no matter what is worse than none because it lies, so make it check the things the app genuinely needs: the database connection, a cache ping, whatever it cannot run without. Then have the deploy script wait on it instead of guessing with a fixed sleep:
#!/usr/bin/env bash
set -euo pipefail
NEW_PORT=8082
systemctl start app@"$NEW_PORT"
# wait up to 30s for the new release to report healthy
for i in $(seq 1 30); do
if curl -fsS "http://127.0.0.1:$NEW_PORT/healthz" >/dev/null; then
echo "healthy"; break
fi
sleep 1
if [ "$i" -eq 30 ]; then echo "unhealthy, aborting"; exit 1; fi
done
5. Drain and switch
Point the proxy at the healthy new release and reload. This is the actual moment of the deploy, and the reason it is safe is that nginx does not slam the door. It finishes the requests already in flight on the old workers before retiring them, so nothing in progress gets cut off:
sed -i "s/:8081/:$NEW_PORT/" /etc/nginx/conf.d/app.conf
nginx -t && systemctl reload nginx # graceful, connection-draining reload
6. Smoke test in production
The second the switch lands, run a few read-only checks against the live URL. Load the homepage, hit one core API route, fetch a static asset. It takes ten seconds, and it means you learn the release is bad from your own terminal rather than from a customer email forty minutes later.
7. Watch, then keep rollback one command away
Stay on the error rate and p95 latency for about ten minutes; most bad deploys announce themselves inside the first five. If one does, rollback is not a scramble, because the previous artifact is still sitting on disk and the proxy switch was a single edit, so undoing it is the same move backwards. Wire the whole sequence into CI so a deploy, and a rollback, is one button:
# .github/workflows/deploy.yml (excerpt)
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build artifact
run: ./scripts/build.sh "$GITHUB_SHA"
- name: Ship (health-checked, draining switch)
run: ./scripts/deploy.sh "$GITHUB_SHA"
The mindset shift
Once every deploy is reversible and every deploy is observable, the fear quietly drains out of shipping. You start pushing small changes often, in daylight, with someone watching the dashboard next to you, because a bad release is a ten-second rollback instead of a ruined evening. That shift in how the team feels about deploying is worth more than any single tool in the pipeline.