Playbook

Configure Python App

A service account, Gunicorn settings and a hardened systemd service for one Python web app.

Category

Runtimes

Time limit per server

5 minutes

Temporary failures

Not retried: run it again

Version

1.0

Supported systems

Ubuntu 22.04+, Debian 12+, RHEL-compatible 8+ (AlmaLinux, Rocky Linux, RHEL, CentOS Stream, Oracle Linux), Amazon Linux 2023+

Options

As they appear on the run form, with their defaults.

Application name

Text

Lowercase letters, digits and hyphens, such as api. Names the folder, account and service.

Default: Required

Application module

Text

What Gunicorn loads, such as config.wsgi:application for Django or app.main:app for FastAPI.

Default: Required

Application interface

One choice

  • WSGI: Django, Flask
  • ASGI: FastAPI, Starlette, Django over ASGI

Default: WSGI: Django, Flask

Port on 127.0.0.1

Text

Gunicorn listens here, on this server only.

Default: 8000

When to use it

After Install Gunicorn, to give a Python web application everything it needs on the server except its code: an account to run as, Gunicorn's settings and a systemd service. The service is installed but not started until you deploy.

Choosing the options

Application name — the same name you gave Install Gunicorn, such as api. For api it creates the api account, /srv/api, /etc/api and the api service.

Application module — what Gunicorn loads, in module:variable form:

  • Django over WSGI: config.wsgi:application, where config is the folder holding settings.py.
  • Django over ASGI: config.asgi:application.
  • FastAPI: app.main:app when the file is app/main.py and the application object is app.
  • Flask: app:app when the file is app.py.

Application interface — the same choice as in Install Gunicorn: WSGI for Django and Flask, ASGI for FastAPI and Starlette.

Port on 127.0.0.1 — the port Gunicorn listens on, on this server only, between 1024 and 65535. 8000 unless something else uses it.

What it does on the server

The examples below use an application named api on port 8000.

  1. Creates a system account and group named after the application. The account cannot sign in, and /srv/api is readable only by root and that account.
  2. Writes /etc/api/gunicorn.conf.py: listening on 127.0.0.1:8000, sync workers sized as twice the CPU count plus one for WSGI, or Uvicorn workers sized as the CPU count plus one for ASGI, a 30-second request timeout, worker recycling after about 1,000 requests, forwarded headers trusted from this server only, and logs to the journal.
  3. Writes /etc/systemd/system/api.service: Gunicorn from /srv/api/venv, working in /srv/api/current, run as the api account, restarted if it fails, unable to write to the system or read home directories. Environment variables are read from /etc/api/environment when it exists.
  4. Checks the unit with systemd and the configuration with Python. The service is not started while there is no code.

After the run

For the api application on port 8000:

  1. Deploy your code to /srv/api/current.
  2. Install its requirements into the environment: /srv/api/venv/bin/pip install -r /srv/api/current/requirements.txt.
  3. Put its settings in /etc/api/environment, one KEY=value per line, owned by root with mode 0640 and group api.
  4. Start it with systemctl enable --now api, and check it answers with curl http://127.0.0.1:8000/.
  5. Put HTTPS in front with Configure Reverse Proxy, forwarding to http://127.0.0.1:8000.

Common problems

"Port … is already in use by …" — Choose another port. Nothing was changed.

"… already exists and was not written by this playbook." — A service with that name exists. Choose another application name, or remove the existing unit if it is no longer used.

"An account called … already exists and belongs to a person" — The name is taken by a user account. Choose another application name.

"… does not exist. Run Install Gunicorn for … first." — Run Install Gunicorn with the same application name first.

The service starts and then stops — Read its log, for api with journalctl -u api -n 50. A wrong Application module, missing requirements, or a setting the application expects in /etc/api/environment are the usual causes.