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

bash
# 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 || true

Adapt 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.

bash
ping -c 2 yourdomain.com || true
dig yourdomain.com +short
df -h
free -m
ss -ltnp

Check:

  • server is reachable
  • DNS resolves to the correct IP
  • firewall allows 80 and 443
  • 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.

bash
python --version
pip --version
node --version || true
npm --version || true
which gunicorn || true
which nginx

For virtualenv-based deploys:

bash
source /var/www/app/venv/bin/activate
python -m pip install -r requirements.txt

For Docker-based deploys:

bash
docker compose config
docker compose pull

3. Validate environment variables

Keep production config in environment variables, not source files.

Minimum example:

bash
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)
PY

Also 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.

bash
python - <<'PY'
import os
print(os.getenv("DATABASE_URL"))
PY

Framework-specific checks:

bash
python manage.py check --deploy 2>/dev/null || true
alembic current 2>/dev/null || true
python manage.py showmigrations 2>/dev/null || true

If your app has a health endpoint with DB verification, test it directly after startup:

bash
curl -I http://127.0.0.1:8000/health

5. 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:

bash
git fetch --all --tags
git checkout <tag-or-commit>

Docker-based deploy:

bash
docker compose pull
docker images | head

Avoid deploying from uncommitted local changes.

7. Install dependencies with locked versions

Use pinned dependencies.

bash
python -m pip install -r requirements.txt
npm ci

If dependency install changes unexpectedly during deploy, stop and inspect version drift.

8. Run tests or smoke checks

At minimum, validate startup and critical paths.

bash
pytest -q

Minimum 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:

bash
alembic upgrade head
bash
python manage.py migrate

If 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:

bash
python manage.py collectstatic --noinput

Node frontend example:

bash
npm ci
npm run build

Verify files exist where Nginx or the app expects them.

bash
ls -lah /var/www/app/static
ls -lah dist 2>/dev/null || true

11. Restart app services

Restart web, workers, and schedulers, not just the web process.

Systemd example:

bash
sudo systemctl restart gunicorn
sudo systemctl restart celery || true
sudo systemctl restart rq-worker || true
sudo systemctl restart celerybeat || true

Validate status:

bash
systemctl status gunicorn --no-pager
systemctl status celery --no-pager

Docker Compose example:

bash
docker compose up -d --remove-orphans
docker compose ps

12. Validate and reload the reverse proxy

Never reload Nginx without config validation.

bash
nginx -t
sudo systemctl reload nginx

Basic Nginx reverse proxy shape:

nginx
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.

bash
dig yourdomain.com +short
curl -I https://yourdomain.com
curl -I https://yourdomain.com/health

Check:

  • 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
git/tag
build
migrate
restart
health check
monitor

Process Flow

15. Watch logs and monitoring for 5–15 minutes

Check for elevated errors, worker crashes, 502s, and DB failures immediately after release.

bash
journalctl -u gunicorn -n 200 --no-pager
journalctl -u nginx -n 200 --no-pager
journalctl -u celery -n 200 --no-pager

Docker:

bash
docker compose logs --tail=200 web
docker compose logs --tail=200 nginx

Then 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:

bash
git checkout <previous-tag>
sudo systemctl restart gunicorn
sudo systemctl reload nginx
bash
docker compose pull web:<previous-tag>
docker compose up -d

Rollback 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_URL or SECRET_KEY
  • wrong service user
  • stale IP in DNS
  • no previous image tag or release artifact available for rollback

Debugging tips

Check services first:

bash
systemctl status nginx
systemctl status gunicorn
systemctl status celery

Inspect logs:

bash
journalctl -u nginx -n 200 --no-pager
journalctl -u gunicorn -n 200 --no-pager
journalctl -u celery -n 200 --no-pager

Validate Nginx and internal app connectivity:

bash
nginx -t
curl -I http://127.0.0.1:8000
curl -I https://yourdomain.com
curl -I https://yourdomain.com/health

Check DNS, ports, and system resources:

bash
dig yourdomain.com +short
ss -ltnp
df -h
free -m

Inspect processes and env assumptions:

bash
ps aux | grep -E 'gunicorn|uvicorn|celery|rq'
python -c "import os; print(os.getenv('DATABASE_URL'))"
alembic current || python manage.py showmigrations

Docker-specific checks:

bash
docker compose ps
docker compose logs --tail=200 web
docker compose logs --tail=200 nginx

Debugging sequence:

  1. confirm app process is up
  2. confirm app responds on localhost
  3. confirm reverse proxy forwards correctly
  4. confirm DNS and TLS
  5. confirm schema version matches app expectations
  6. confirm workers restarted and queues are moving

Use this quick command matrix when triaging a failing deployment:

ItemCommandExpected resultRollback impact if failing
App process healthsystemctl status gunicorn --no-pagerService is active (running)High: app may be unavailable
Local app reachabilitycurl -I http://127.0.0.1:8000HTTP 200/302 from appHigh: reverse proxy cannot recover this alone
Reverse proxy confignginx -tsyntax is ok and test is successfulMedium: fix config before reload
Public HTTPS routecurl -I https://yourdomain.com/healthHealthy response (2xx) over TLSHigh: users impacted externally
Migration state`alembic currentpython manage.py showmigrations`
Worker processingsystemctl status celery --no-pager or rq infoWorker running and queues movingMedium/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

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.