Skip to content

Reference Setup

How an entirius-service-* runs in production. This is the reference setup the maintainers run; install and update scripts are not shipped yet. The service repo ships the application (main/wsgi.py, main/celery.py, main/settings_example.py); host layout, process units and scripts are yours to write, and these pages say what they must do. Everything CI/CD and Releases cover ends at “the package is published”; this section starts where they stop.

The setup is the native deployment rail the maintainers run. It is a recommendation, with the reason next to each rule. A deviation is fine; document it in your service repository.

PageCovers
Reference Setup, belowhost layout, the deploy rail, the post-deploy gate
Process Supervisionsupervisor programs for uwsgi, celery and beat: stop signals, group-kill, timeouts
Watchdogsdetectors for faults nothing else reports, and how to arm them without crying wolf
Deploy Patternsevery part of a deployment and the file it is configured in

One service, one host (an LXC container or a VM), everything on the box:

ComponentRuns asListens on
nginxsystem service:80, the only public port
uwsgi (Django)supervisor programunix socket in run/
celery workersupervisor programnone
celery beatsupervisor programnone
PostgreSQL, Redis, RabbitMQsystem services127.0.0.1 only

Why native and not containers: one host per environment has nothing to schedule, and a container runtime on top of an LXC adds a layer to debug without removing one. Docker belongs to local development. No WSGI server is a dependency of the service (pyproject.toml); the reference runs uWSGI. The integration harness (entirius-zeno) is not a deployment target.

Why supervisor and not systemd units: one config format for all three Python processes, and supervisorctl status as the single view of the app. The price is that supervisor’s defaults are unsafe for forking servers, see Process Supervision. That page is mandatory reading before touching a unit template.

The rail has three jobs, and every step is idempotent (re-running converges the host):

  • Converge applies the checkout it is given: system packages, the database, the venv from uv.lock with uv sync --frozen, migrations, static files, process units, and a restart.
  • Quick converge is the routine redeploy for code-only changes: venv, migrations, static files, units, restart.
  • Fetch is a separate step (git pull --ff-only). Converge never moves the checkout, so a host can be converged onto a reviewed commit.
  • Keep every generated file as a template in your repo: settings_local.py, supervisor programs, the uWSGI ini, nginx, cron. The converge step expands the placeholders. A hand edit on the host is erased by the next deploy, by design: the repo is the only place a change survives.
  • Feed every consumer from one env file. An operator-edited source file is expanded into one generated file (640 root:<app-user>), which supervisor, cron and uWSGI all read. Three copies of a config drift; one cannot.
  • Put operator flags in the source file, never the generated one. Anything set in the generated file by hand is wiped by the next converge, silently.
  • Generate secrets once, on the host, never in git. The first converge writes them to a 600 root file; later runs read them back.
  • Install dependencies from uv.lock. uv sync --frozen, PyPI only, no git dependencies, no build-time credentials.

A deploy that exits 0 on an app that serves nothing is worse than a failed one. The converge ends with a smoke step and exits non-zero if any check fails:

  1. make health in the service checkout: manage.py check, the database check and migrate --check. It is read-only and safe in production.
  2. supervisorctl status: no program in FATAL, BACKOFF or EXITED.
  3. The process tree is the expected one: one uwsgi master, exactly processes workers under it, zero uwsgi processes parented to init (see Process Supervision).
  4. An HTTP request through nginx returns the expected status.
  5. An API error path returns the mapped 4xx envelope, not a 500. A happy path alone does not prove the exception handler is wired.
  6. One real call through every external integration the service depends on (a proxy, a third-party API). Configuration that is present but wrong only shows up here.

Then run the configuration health check.

Deploy templates are code and get tests in the service’s own suite, so they run in CI, long before a host sees the change. At minimum:

  • every rail script exists, is executable, and parses (bash -n);
  • every placeholder in a template is one the converge step expands;
  • the supervision invariants from Process Supervision.

Process Supervision