DevOps Practices·8 min read·

The Twelve-Factor App: How Each Factor Maps to a Real VPS Deployment

A factor-by-factor walkthrough that turns the Twelve-Factor methodology into concrete choices for deploying an app on a single Linux VPS of your own.

NB

Netbay Engineering

Netbay Engineering

On this page

The Twelve-Factor App has been the default playbook for building deployable services since 2011, and most of its advice is written for teams with orchestration platforms and pooled infrastructure. You do not need any of that to benefit from it. Every factor maps onto a concrete decision you make on a single VPS: where configuration lives, how the process starts, what happens on redeploy, where logs go. This post walks through all twelve factors and shows the exact files and commands that implement each one on an everyday Linux server.

Factors 1-3: one codebase, explicit dependencies, config in the environment

Factor one says one codebase per deployable service, tracked in version control, deployed many times. On a VPS this kills the oldest bad habit in server administration: editing files in place. If /srv/myapp contains code that exists nowhere in git, you no longer have a deployable app — you have a snowflake. Factor two demands explicit dependency declarations: a lockfile committed to the repo, dependencies installed into a virtualenv or node_modules owned by the app, never scattered into the system package manager by hand.

Factor three has the highest payoff per line of effort: store config in the environment. Anything that varies between deploys — database URLs, credentials, log levels — must never be hardcoded. The simplest honest implementation is a .env file outside the repo, loaded by the init system:

bash
# /srv/myapp/.env -- owned by myapp:myapp, mode 600, never committed
DATABASE_URL=postgres://myapp:not-the-real-password@127.0.0.1:5432/myapp
REDIS_URL=redis://127.0.0.1:6379/0
LOG_LEVEL=info
BIND_HOST=127.0.0.1
BIND_PORT=8000
python
import os

# read config strictly: crash at boot if a required variable is missing
DATABASE_URL = os.environ["DATABASE_URL"]
LOG_LEVEL = os.environ.get("LOG_LEVEL", "info")

Strict reads matter. An app that boots with a missing variable and only discovers the problem on the first request has converted a config error into a 3 a.m. page. Failing at startup is the cheap place to fail.

Factors 4-6: backing services, the build-release-run split, disposability

Factor four treats databases, caches, and queues as attached resources addressed by a URL. Swap the DATABASE_URL and the same code runs against a different Postgres; nothing in the app knows or cares where the database physically lives. Factor five splits delivery into three strictly separated stages, which is exactly where hand-rolled deployments fall apart.

BUILD repo + locked deps + tests artifact RELEASE artifact + env config same bits RUN processes on the VPS factors 1-2 codebase, dependencies factors 3-5 config, backing services, parity factors 6-9 processes, ports, scale, dispose factors 10-12 span every stage dev-prod parity, logs to stdout, admin tasks as one-off processes build once, configure at release, run anywhere on the server

In practice the split means you never build on the production box. Build an artifact in CI or on a build host, store it, and let deployment be nothing more than copying the artifact, writing config, and restarting. Factor six says processes are disposable: they start in seconds and shut down gracefully on SIGTERM. Both properties come almost free from systemd if you configure them deliberately.

Factors 7-9: port binding, scaling by process count, fast starts

Factor seven: the app listens on its own port instead of assuming a particular web server hosts it. Gunicorn binding to 127.0.0.1:8000 is the whole contract; nginx sits in front as a reverse proxy and nothing else needs to know how the app is built. Factor eight scales horizontally by running more processes, not by growing one giant process — that is exactly what the -w flag does. Factor nine is disposability from the init system's point of view: robust restart handling, short graceful shutdown, crash-only thinking.

ini
# /etc/systemd/system/myapp.service
[Unit]
Description=myapp web service
After=network.target

[Service]
User=myapp
WorkingDirectory=/srv/myapp
EnvironmentFile=/srv/myapp/.env
ExecStart=/srv/myapp/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=15

[Install]
WantedBy=multi-user.target

Factors 10-12: parity, logs, admin processes

Factor ten, dev-prod parity, does not mean buying identical hardware. It means the same OS family, the same runtime versions, and the same deployment path in every environment. An inexpensive staging VPS running the same Ubuntu 24.04 release as production catches the version drift that byte-identical code still suffers.

Factor eleven treats logs as event streams. The app writes unbuffered lines to stdout and nothing else; the init system owns capture and rotation, and anything smarter ships the stream onward. Factor twelve says admin and maintenance tasks — migrations, one-off scripts, console shells — run as one-off processes in the same release with the same config, not as special scripts living in someone's home directory.

bash
# the app only ever writes to stdout; journald owns capture
journalctl -u myapp -f --since today

# an admin process: same codebase, same release environment
sudo -u myapp /srv/myapp/venv/bin/python /srv/myapp/manage.py migrate

Summary: The 12-Factor Checklist for Single-VPS Hosts

  1. **Codebase**: Tracked in git, cloned into versioned releases.
  2. **Dependencies**: Locked in file, isolated in virtualenv / container.
  3. **Config**: Environment variables loaded via .env and systemd.
  4. **Backing Services**: Attached resources reachable via connection strings.
  5. **Build, Release, Run**: Clean separation between artifact build and production launch.
  6. **Processes**: Stateless and share-nothing; state stored in backing datastores.
  7. **Port Binding**: Export services via direct HTTP/TCP listening.
  8. **Concurrency**: Scale out via process model (worker threads/processes).
  9. **Disposability**: Fast startup and graceful SIGTERM handling.
  10. **Parity**: Keep staging and production environments aligned.
  11. **Logs**: Treat as event streams directed to stdout/journald.
  12. **Admin Processes**: Run management tasks against the active release environment.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance