
A Linux web application needs a clear path from the public request to the application process, plus a way to restart, observe, update, and recover that process. The architecture can be small while still making those responsibilities explicit.
This worked example targets a dedicated Debian 13 host using Python, Gunicorn, systemd, and Caddy. It serves a tiny read-only application with a health endpoint. It has no login, database, uploads, or user data; those features require additional design. Use a test host before adapting the example to an existing server.
Give each layer one clear job
On a small screen, scroll the table sideways to read all columns.
| Layer | Job | Boundary |
|---|---|---|
| Caddy reverse proxy | Accept the public request and manage HTTPS for the configured hostname. | Only the proxy needs public web ports. |
| Gunicorn application server | Run the Python WSGI application on 127.0.0.1:8000. | The application listener stays on the host’s loopback interface. |
| systemd service | Start the process with a dedicated account and restart it after failure. | A service manager does not prove that a response is correct. |
| Application and persistent data | Implement behavior; keep any future writable data outside replaceable code. | This teaching application has no persistent data to back up. |
A reverse proxy forwards requests to an application server; it does not supply the application’s authorization or input validation. Gunicorn’s deployment documentation discusses proxying and process supervision. This sample uses threaded workers rather than relying on the default synchronous worker behavior. Worker and timeout settings are starting assumptions to test against the real workload.
Start with a small, inspectable application
Download the example application, service file, Caddyfile, and setup notes (ZIP). The Python file returns a greeting at /, ok at /healthz, 404 for an unknown path, and 405 for an unsupported method. It accepts GET and HEAD. A healthy response here proves only that this process can answer; a real application may need a separate readiness check for required dependencies.
The files are intended for review and installation by an administrator. The archive contains no automatic installer. The application’s HTTP responses and local reverse-proxy path were tested; Linux service startup, reboot behavior, and public certificate issuance must be validated on the target host.
On a dedicated Debian 13 test host, an administrator can install the distribution packages with apt install python3 gunicorn caddy curl after refreshing package information. Check configured repositories and the proposed package changes first. For framework applications with extra dependencies, use an isolated environment and a reviewed dependency lock; do not overwrite the distribution’s Python environment with a global package-manager installation.
Run the application under its own account
Create the non-login yenra-web-demo system account, install the reviewed app.py in /opt/yenra-web-demo/, and keep that directory owned by the administrator. The README gives the commands and file permissions. Save the supplied unit as /etc/systemd/system/yenra-web-demo.service. Its key settings are:
[Service]
User=yenra-web-demo
Group=yenra-web-demo
WorkingDirectory=/opt/yenra-web-demo
ExecStart=/usr/bin/gunicorn --workers 2 --worker-class gthread --threads 4 --bind 127.0.0.1:8000 --access-logfile - --error-logfile - app:application
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
This is an excerpt; install the complete unit from the download, including its Unit and Install sections. The systemd execution-environment manual explains the filesystem and privilege restrictions. These controls can break an application that expects to write beside its code. Give real applications a specifically managed writable data location instead of relaxing every restriction.
Verify the unit, reload systemd’s configuration, start the service, and check it locally:
sudo systemd-analyze verify /etc/systemd/system/yenra-web-demo.service
sudo systemctl daemon-reload
sudo systemctl enable --now yenra-web-demo
curl --fail http://127.0.0.1:8000/healthz
sudo journalctl -u yenra-web-demo -n 50 --no-pager
Expected health response: ok. Confirm that the listener is loopback-only. If startup fails, inspect the first relevant error: an incorrect executable path, missing account, unreadable file, or import failure needs a different fix from an occupied port.
Add the public hostname at the proxy
On a fresh test host, replace the package’s default Caddy site with the supplied example after keeping a copy of its configuration. On a shared host, integrate a distinct site block without overwriting existing sites. Replace app.example.com with a hostname you control:
app.example.com {
reverse_proxy 127.0.0.1:8000
}
Point the hostname’s DNS records to the intended server, including any IPv6 record. Permit the required public web traffic through host and network firewalls while keeping the application port private. Caddy’s automatic HTTPS documentation explains certificate automation and its prerequisites. The reserved example hostname cannot obtain a certificate for your deployment.
Run sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile, then reload the Caddy service. Test https://your-hostname/healthz from outside the server. A valid configuration does not prove that DNS, firewall rules, certificate issuance, or the upstream connection work. If the proxy returns 502, check the application’s loopback response and service logs before changing DNS.
Test failure and keep a rollback route
Verify normal requests, unknown paths, unsupported methods, service restart, and a controlled reboot on the test host. Stop the application briefly to confirm that monitoring notices failure, then restore it. Monitor the public path as well as the local health endpoint. Decide who receives an alert and what response is expected.
Keep a known-good application release and configuration. For a simple replacement, stop the service, install the reviewed code, start it, and verify health and behavior; schedule the interruption. Rolling back code may be enough for this stateless example. It is not enough after an incompatible database migration, so real applications need data-aware deployment and recovery plans.
Keep secrets out of source archives and logs. Plan log access, retention, and disk usage. Back up persistent application data with a tested restore process, and preserve the configuration needed to rebuild the service. Containers change packaging and isolation, but the same duties remain: process health, network boundaries, persistent storage, updates, and recovery all need owners.