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