Deployment.sh

Web application deployment checklist for real production servers

A web application deployment that works once is easy. One that works every week for years, survives a bad release at 6 pm on a Friday and lets a new developer ship on day two takes a checklist. This is the one we would hand to any team putting an app on its own servers, whatever the stack. It is grouped by area, each item is something you can verify, and each one exists because skipping it has broken a production system somewhere.

Top-down view of a developer's desk with a laptop terminal, a notebook with a hand-sketched diagram of boxes and arrows and a hardware security key

Server baseline

The server is the foundation. If it is not locked down and predictable, nothing above it is.

  • SSH login with keys only, password authentication and root login disabled
  • A firewall that allows only SSH, HTTP and HTTPS, plus any port another server of yours needs, from that server's IP
  • Unattended security updates enabled for the operating system
  • A swap file on small servers, so builds and spikes do not trigger the out of memory killer
  • Time synchronization running, because TLS, logs and scheduled jobs depend on correct clocks
  • A dedicated non root user per app or per team, owning the app files
  • Disk usage alerts, because a full disk takes down databases, logs and deploys at the same time

Write the baseline as a script or use a tool that applies it, so the next server looks exactly like the first.

bash
ufw default deny incoming
ufw allow OpenSSH && ufw allow 80 && ufw allow 443
ufw enable
dpkg-reconfigure -plow unattended-upgrades

Runtime and web server

Pin the runtime version per app. A distribution upgrade or a shared runtime that moves on its own is a classic cause of a deploy that worked yesterday and fails today.

  • The runtime version is declared in the repository (.nvmrc, .python-version, composer.json platform, Gemfile ruby line, go.mod, global.json)
  • nginx or Caddy sits in front of the app and terminates TLS
  • HTTPS is enforced with a redirect, and HSTS is on once you are sure every subdomain supports TLS
  • Certificates renew automatically and you get an alert when renewal fails
  • Gzip or brotli compression and long cache headers on hashed static assets
  • Request body size and timeouts are set deliberately, not left at defaults that surprise you on the first file upload

The release process

This is where most of the pain lives. A good web application deployment process has a few properties you can check directly.

  • Every deploy starts from a commit in git, never from files edited on the server
  • The build runs in a new release folder, not in the live directory
  • Shared state (uploads, .env, storage, caches you want to keep) is linked into each release
  • A health check runs before traffic moves to the new release
  • The switch to the new release is atomic, a symlink rename or an upstream change
  • The last several releases stay on disk, so rollback takes seconds
  • Only one deploy can run per app at a time, enforced by a lock
  • Every deploy writes a log with the commit, the author, the output and the result

If you only fix one thing from this checklist, fix the release folder and the atomic switch. Our guide on zero downtime deployment strategies covers the options in detail.

Configuration and secrets

Configuration mistakes cause as many incidents as code. Treat environment variables as a deployable artifact with the same care as code.

  • Secrets live outside the repository, in a file or store per environment
  • Files holding secrets have tight permissions, for example 600 owned by the app user
  • Staging and production never share credentials, databases or storage buckets
  • Changes to environment variables are reviewed, with a diff, before they go live
  • Debug modes and verbose error pages are off in production
  • Every secret can be rotated without a code change

A simple test: could a new team member accidentally point staging at the production database? If yes, the configuration is not separated enough.

Database and migrations

The database outlives every release, so changes to it need the most care.

  • The app connects with its own database user, with grants limited to its own database
  • The database listens on localhost or a private network, never on a public interface without a firewall rule
  • Migrations run as part of the deploy, before the traffic switch, and a failed migration stops the deploy
  • Migrations are backward compatible for one release (expand first, contract later)
  • Large index builds and table rewrites use online methods for your database engine
  • A backup runs before any migration that drops or rewrites data
bash
pg_dump -Fc myapp > /var/backups/myapp-$(date +%F-%H%M).dump

Workers, schedulers and background jobs

Background processes are part of the app and must follow the same release.

  • Queue workers run under a supervisor (systemd or supervisor) and restart on crash
  • Workers restart on every deploy, with a graceful stop so jobs in progress finish
  • Exactly one scheduler runs, so scheduled tasks do not fire twice
  • Cron jobs write their output to a log you can read, not to /dev/null
  • Job payloads stay readable by both the old and the new release during a deploy

Backups and restore

A backup you have never restored is a hope, not a backup.

  • Databases are backed up on a schedule that matches how much data you can afford to lose
  • Uploaded files are backed up too, or stored in object storage with versioning
  • Backups go to storage outside the server and outside the server's provider account if you can
  • Retention is defined, so old backups are pruned and storage costs stay predictable
  • A restore is tested regularly, end to end, into a separate database

Monitoring, logs and alerts

You should learn about a problem from an alert, not from a customer.

  • CPU, memory, disk and load are tracked per server with history
  • An HTTP uptime check hits a health URL that touches the database
  • Application logs, web server logs and deploy logs are easy to reach for everyone on call
  • Alerts go to a channel people actually watch, email plus team chat
  • Failed deploys raise an alert just like a down server does

Access and accountability

Once more than one person deploys, access control matters.

  • Each person has their own account and SSH key, no shared logins
  • Roles separate who can deploy, who can change environment variables and who can only view
  • Access to a server can be granted and revoked per person
  • An audit log records who changed what and when
  • Former team members lose access the same day they leave

Using this web application deployment checklist

Go through the list for each app once, mark what is missing, and fix the items in this order: release process, backups, secrets, monitoring, then everything else. The first two save you from the incidents that cost days. The rest saves you from the incidents that cost hours.

How Deployment covers the checklist

Deployment turns most of this list into defaults on servers you own. It provisions every server with a firewall, SSH hardening, unattended security updates and swap, installs nginx or Caddy and the runtime your repository declares, and issues Let's Encrypt certificates that renew on their own. Each push builds in a new release folder with a health check before the switch, keeps earlier releases for one click rollback and locks deploys per app. Environment variables are encrypted per environment with a diff before each deploy, workers and schedulers restart with every release, backups go to your own S3-compatible storage with retention and restore, and monitoring, uptime checks, alerts, roles and an audit log are built in. See the software deployment automation overview, or paste a public repository into the demo on the home page to see the server plan and deploy script for your app.