Deployment Checklist
The essential playbook for implementing deployment checklist in your SaaS.
Use this checklist before every production release. It is built for small SaaS apps deployed on a VPS, Docker host, or simple cloud instance. The goal is to reduce failed deploys, broken migrations, missing environment variables, downtime, and post-release surprises.
Quick Fix / Quick Setup
# Pre-deploy validation checklist
export APP_ENV=production
set -a && source .env && set +a
# 1) Confirm required env vars exist
python - <<'PY'
import os, sys
required = [
'DATABASE_URL', 'SECRET_KEY', 'ALLOWED_HOSTS'
]
missing = [k for k in required if not os.getenv(k)]
print('MISSING:', missing)
sys.exit(1 if missing else 0)
PY
# 2) Install deps and run app checks
python -m pip install -r requirements.txt
pytest -q || exit 1
# 3) Run migrations before restart
python manage.py db upgrade 2>/dev/null || alembic upgrade head 2>/dev/null || true
# 4) Collect/build static assets if applicable
python manage.py collectstatic --noinput 2>/dev/null || true
npm ci && npm run build 2>/dev/null || true
# 5) Validate reverse proxy and app server config
nginx -t
sudo systemctl restart gunicorn 2>/dev/null || sudo systemctl restart app 2>/dev/null || true
sudo systemctl reload nginx
# 6) Health check after deploy
curl -I https://yourdomain.com
curl -I https://yourdomain.com/health || true
# 7) Tail logs for immediate failures
journalctl -u gunicorn -n 100 --no-pager 2>/dev/null || true
journalctl -u nginx -n 100 --no-pager 2>/dev/null || trueAdapt the migration command to your framework. If you use Docker, replace service restarts with docker compose pull/build/up and run checks inside the container.
What’s happening
Production deploys fail when one or more release steps are skipped or run in the wrong order.
Most issues come from config drift between development and production:
- missing env vars
- invalid process manager config
- broken DNS
- SSL not provisioned
- migrations incompatible with current code
A deployment checklist standardizes the release process so every deploy includes validation, backup awareness, migration sequencing, service restarts, health checks, monitoring, and rollback readiness.
For small SaaS products, a simple repeatable process is better than a complex deployment platform you do not fully control.
Step-by-step implementation
1. Confirm infrastructure is ready
Verify the server, network, and host capacity before touching the app.
ping -c 2 yourdomain.com || true
dig yourdomain.com +short
df -h
free -m
ss -ltnpCheck:
- server is reachable
- DNS resolves to the correct IP
- firewall allows
80and443 - enough disk and memory are available
If you still need the server baseline, start with Environment Setup on VPS.
2. Verify runtime dependencies
Make sure the deployed host has the expected runtime versions and package managers.
python --version
pip --version
node --version || true
npm --version || true
which gunicorn || true
which nginxFor virtualenv-based deploys:
source /var/www/app/venv/bin/activate
python -m pip install -r requirements.txtFor Docker-based deploys:
docker compose config
docker compose pull3. Validate environment variables
Keep production config in environment variables, not source files.
Minimum example:
export APP_ENV=production
set -a && source .env && set +a
python - <<'PY'
import os, sys
required = [
"DATABASE_URL",
"SECRET_KEY",
"ALLOWED_HOSTS",
]
missing = [k for k in required if not os.getenv(k)]
print("missing:", missing)
sys.exit(1 if missing else 0)
PYAlso verify any feature-specific secrets:
- auth provider keys
- email provider credentials
- storage bucket config
- webhook secrets
- payment provider secrets
Related checklists:
4. Verify database connectivity
Confirm the app host can reach the production database before deploy.
python - <<'PY'
import os
print(os.getenv("DATABASE_URL"))
PYFramework-specific checks:
python manage.py check --deploy 2>/dev/null || true
alembic current 2>/dev/null || true
python manage.py showmigrations 2>/dev/null || trueIf your app has a health endpoint with DB verification, test it directly after startup:
curl -I http://127.0.0.1:8000/health5. Confirm backup and restore readiness
Before schema changes, know your restore path.
Minimum checks:
- automated DB backup exists
- last successful backup timestamp is recent
- restore command is documented
- previous app release artifact or image tag is available
If you cannot restore, you are not ready for risky migrations.
6. Pull or build the release artifact
Use a tagged release, pinned commit, or immutable image.
Git-based deploy:
git fetch --all --tags
git checkout <tag-or-commit>Docker-based deploy:
docker compose pull
docker images | headAvoid deploying from uncommitted local changes.
7. Install dependencies with locked versions
Use pinned dependencies.
python -m pip install -r requirements.txt
npm ciIf dependency install changes unexpectedly during deploy, stop and inspect version drift.
8. Run tests or smoke checks
At minimum, validate startup and critical paths.
pytest -qMinimum manual or scripted smoke set:
- login works
- dashboard loads
- one DB-backed page loads
- one write action succeeds
- payment callback/auth callback endpoints are reachable if used
9. Run database migrations in the correct order
Prefer backward-compatible migrations before switching traffic.
Examples:
alembic upgrade headpython manage.py migrateIf the release contains schema changes that break old code or require immediate code coupling, use expand-and-contract migration strategy instead of a single hard cut.
See also: Database Migration Strategy
10. Build static or frontend assets
Make sure built output matches your server config.
Django example:
python manage.py collectstatic --noinputNode frontend example:
npm ci
npm run buildVerify files exist where Nginx or the app expects them.
ls -lah /var/www/app/static
ls -lah dist 2>/dev/null || true11. Restart app services
Restart web, workers, and schedulers, not just the web process.
Systemd example:
sudo systemctl restart gunicorn
sudo systemctl restart celery || true
sudo systemctl restart rq-worker || true
sudo systemctl restart celerybeat || trueValidate status:
systemctl status gunicorn --no-pager
systemctl status celery --no-pagerDocker Compose example:
docker compose up -d --remove-orphans
docker compose ps12. Validate and reload the reverse proxy
Never reload Nginx without config validation.
nginx -t
sudo systemctl reload nginxBasic Nginx reverse proxy shape:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location /static/ {
alias /var/www/app/static/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}If TLS is not already configured, use HTTPS Setup (Let’s Encrypt).
13. Confirm HTTPS and domain routing
Verify both DNS and TLS.
dig yourdomain.com +short
curl -I https://yourdomain.com
curl -I https://yourdomain.com/healthCheck:
- DNS points to the correct server
- certificate is active
- HTTP redirects to HTTPS
- health endpoint returns success
14. Run post-deploy checks
Do not stop at a 200 OK on the homepage.
Validate:
- homepage
- login
- dashboard
- admin
- webhook endpoint reachability
- file upload
- outbound email trigger
- worker queue processing
Process Flow
15. Watch logs and monitoring for 5–15 minutes
Check for elevated errors, worker crashes, 502s, and DB failures immediately after release.
journalctl -u gunicorn -n 200 --no-pager
journalctl -u nginx -n 200 --no-pager
journalctl -u celery -n 200 --no-pagerDocker:
docker compose logs --tail=200 web
docker compose logs --tail=200 nginxThen confirm your alerting and monitoring are active via Monitoring Checklist.
16. Record release metadata
Store:
- deployed version or image tag
- timestamp
- migration version
- who deployed
- rollback command
Keep this in your repo or runbook, not memory.
17. Be ready to roll back
If failure exceeds acceptable threshold, roll back immediately.
Examples:
git checkout <previous-tag>
sudo systemctl restart gunicorn
sudo systemctl reload nginxdocker compose pull web:<previous-tag>
docker compose up -dRollback must be paired with a database-safe plan. If the migration is not reversible, document exactly what can and cannot be rolled back.
Common causes
Most production deploy failures come from a small set of repeatable mistakes:
- environment variables missing or loaded from the wrong file
- migrations applied after code restart when the new code expects the new schema immediately
- Nginx upstream points to the wrong socket, port, or service name
- Gunicorn/Uvicorn/systemd service file references the wrong working directory or virtualenv
- static assets built locally but not copied to the server or mounted volume
- TLS certificate expired or domain DNS still points to an old host
- background workers not restarted, leaving jobs queued or failing
- insufficient file permissions for logs, sockets, media, or static directories
- database connection settings wrong after infrastructure changes
- no health check or smoke test, so the issue is detected by users first
Additional common failure points:
- missing
DATABASE_URLorSECRET_KEY - wrong service user
- stale IP in DNS
- no previous image tag or release artifact available for rollback
Debugging tips
Check services first:
systemctl status nginx
systemctl status gunicorn
systemctl status celeryInspect logs:
journalctl -u nginx -n 200 --no-pager
journalctl -u gunicorn -n 200 --no-pager
journalctl -u celery -n 200 --no-pagerValidate Nginx and internal app connectivity:
nginx -t
curl -I http://127.0.0.1:8000
curl -I https://yourdomain.com
curl -I https://yourdomain.com/healthCheck DNS, ports, and system resources:
dig yourdomain.com +short
ss -ltnp
df -h
free -mInspect processes and env assumptions:
ps aux | grep -E 'gunicorn|uvicorn|celery|rq'
python -c "import os; print(os.getenv('DATABASE_URL'))"
alembic current || python manage.py showmigrationsDocker-specific checks:
docker compose ps
docker compose logs --tail=200 web
docker compose logs --tail=200 nginxDebugging sequence:
- confirm app process is up
- confirm app responds on localhost
- confirm reverse proxy forwards correctly
- confirm DNS and TLS
- confirm schema version matches app expectations
- confirm workers restarted and queues are moving
Use this quick command matrix when triaging a failing deployment:
| Item | Command | Expected result | Rollback impact if failing |
|---|---|---|---|
| App process health | systemctl status gunicorn --no-pager | Service is active (running) | High: app may be unavailable |
| Local app reachability | curl -I http://127.0.0.1:8000 | HTTP 200/302 from app | High: reverse proxy cannot recover this alone |
| Reverse proxy config | nginx -t | syntax is ok and test is successful | Medium: fix config before reload |
| Public HTTPS route | curl -I https://yourdomain.com/health | Healthy response (2xx) over TLS | High: users impacted externally |
| Migration state | `alembic current | python manage.py showmigrations` | |
| Worker processing | systemctl status celery --no-pager or rq info | Worker running and queues moving | Medium/High depending on async workload |
Checklist
Use this as the final release gate.
Checklist
- ✓ Infrastructure ready: server reachable, firewall open, enough disk/RAM/CPU
- ✓ Domain and DNS correct for production host
- ✓ HTTPS certificate installed and auto-renew configured
- ✓ Production env vars present and verified
- ✓ Database reachable and backup confirmed
- ✓ Dependencies install cleanly with locked versions
- ✓ Tests or smoke checks pass
- ✓ Database migrations reviewed and executed in correct order
- ✓ Static/frontend assets built and served correctly
- ✓ Application process restarted successfully
- ✓ Background workers and schedulers restarted successfully
- ✓ Nginx or reverse proxy config passes validation and reloads cleanly
- ✓ Health endpoint returns success
- ✓ Critical paths tested manually: login, payments, dashboard, file upload, emails if applicable
- ✓ Logging and error tracking active after release
- ✓ Rollback target identified and documented
- ✓ Release version recorded
Related guides
- Base server setup: Environment Setup on VPS
- HTTPS and auto-renewal: HTTPS Setup (Let’s Encrypt)
- Safer schema changes: Database Migration Strategy
- Post-deploy alerts and checks: Monitoring Checklist
- Full production readiness: SaaS Production Checklist
- Security review before launch: Security Checklist
- Auth release validation: Auth System Checklist
FAQ
What should be mandatory in every deployment checklist?
Environment validation, database connectivity, migrations, app restart, reverse proxy validation, health checks, critical path smoke tests, monitoring verification, and rollback instructions.
Can I skip tests for very small releases?
You can reduce scope, but do not skip smoke checks. At minimum validate startup, login, one database read/write path, and one background process if your app uses jobs.
How do I make deployments safer without full CI/CD?
Use a documented command sequence, locked dependencies, a health endpoint, automated backups, tagged releases, and a saved rollback procedure.
What causes most production deployment failures?
Wrong env vars, broken service configuration, schema/code mismatch, missing static assets, and restarting only part of the stack.
What should I verify immediately after deploy?
HTTP response, health endpoint, logs, error tracking, login flow, dashboard load, migrations applied, worker health, and any payment/auth callback endpoints your app depends on.
Should I run migrations before or after restarting the app?
Prefer backward-compatible migrations before full traffic cutover. Avoid deploys where new code requires schema changes that do not exist yet.
Do I need zero-downtime deployment for a small SaaS?
Not always. A short maintenance window can be acceptable early on, but you still need rollback, health checks, and migration discipline.
What is the minimum safe post-deploy test set?
Health endpoint, login, database-backed page, one write action, one background job trigger, and billing/auth callback endpoints if used.
Should deployment checks live in CI or docs?
Both. CI should automate what it can, but operators still need a human-readable runbook for infrastructure checks and rollback.
How do I handle Docker deploys differently?
Validate compose config, build or pull tagged images, run migrations in the container, restart services, and check logs per container.
Final takeaway
A good deployment checklist prevents avoidable outages caused by missed steps and inconsistent environments.
For small SaaS products, the highest-value items are:
- env validation
- migration safety
- service restart checks
- health checks
- monitoring verification
- rollback readiness
Use the same checklist on every release. Update it after every failed deploy, incident, or near miss.