Run Python in Production with systemd

Khimananda Oli 8 min read Programming and Languages
Run Python in Production with systemd

By Khimananda Oli | Last reviewed: August 2026

Running a Python application directly from the command line or via nohup is acceptable for development but dangerous for live traffic. To reliably run Python in production with systemd, you must define a dedicated service unit that handles process supervision, log aggregation, and automatic recovery after crashes. This approach transforms a fragile script into a managed system service integrated with the host operating system. For teams managing infrastructure on Ubuntu, combining this with a proper initial server setup ensures your application starts securely and persists across reboots without manual intervention.

systemd (PID 1)Service SupervisorRestart / WatchdogPython AppGunicorn / UvicornVirtual Env IsolatedDedicated UserjournaldStructured LogsLog Rotationjournalctl Query
Systemd supervises the Python process, handling lifecycle management and forwarding stdout/stderr to journald for centralized logging.

How do you configure a systemd unit file to run Python in production with systemd?

The unit file is the single source of truth for how your application behaves in production. A common mistake I see in audits is using relative paths or running services as root. When you run Python in production with systemd, precision in the [Service] section determines whether your app survives a memory spike or silently fails at 3 AM. Always use absolute paths for the ExecStart binary; systemd does not inherit your shell’s $PATH. Point directly to the Python interpreter inside your virtual environment (e.g., /opt/myapp/venv/bin/gunicorn) to avoid dependency conflicts with system packages.

Essential unit file directives

Create the file /etc/systemd/system/myapp.service with the following configuration. This template assumes a Gunicorn-based WSGI application, but the principles apply equally to FastAPI/Uvicorn or standalone scripts.

[Unit]
Description=MyApp Python Production Service
Documentation=https://docs.myapp.internal
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/venv/bin/gunicorn \
    --bind unix:/run/myapp/socket.sock \
    --workers 4 \
    --timeout 120 \
    wsgi:application
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Security Hardening
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/run/myapp /var/log/myapp
PrivateTmp=true

[Install]
WantedBy=multi-user.target
  • Type=notify: Use this for Gunicorn/Uvicorn when configured with --preload or systemd notification support. It tells systemd the service is "ready" only after the app explicitly signals readiness, preventing upstream proxies from sending traffic too early. For simple scripts without notification support, use Type=simple.
  • EnvironmentFile: Never hardcode secrets in the unit file. Load them from a root-owned, mode-0600 file. This aligns with environment variable best practices and keeps credentials out of systemctl cat output.
  • Restart=on-failure: Automatically restarts the process if it exits with a non-zero code or is killed by a signal. Avoid always unless you have external rate-limiting, as a misconfigured app can enter a crash loop that consumes CPU.
  • ProtectSystem=strict: Mounts the entire filesystem read-only except for paths listed in ReadWritePaths. This contains breaches; if an attacker exploits your Python app, they cannot modify system binaries or libraries.

What are the critical security hardening steps for Python systemd services?

Security is not optional when you manage production workloads. In my experience helping Nepali fintechs and global SaaS companies pass SOC 2 audits, the most frequent finding is over-privileged service accounts. Running Python as root is never acceptable. Create a dedicated system user with no home directory and no shell access:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo mkdir -p /opt/myapp /run/myapp
sudo chown -R myapp:myapp /opt/myapp /run/myapp

Beyond user isolation, leverage systemd’s built-in sandboxing. These directives cost nothing in performance but significantly reduce your attack surface:

DirectivePurposeProduction Recommendation
NoNewPrivileges=truePrevents child processes from gaining elevated privileges via setuid/setgid binaries.Always enable. Blocks common privilege escalation vectors.
PrivateTmp=trueGives the service its own isolated /tmp namespace.Always enable. Prevents symlink attacks and temp file data leaks.
ProtectHome=trueMakes /home, /root, and /boot inaccessible.Enable unless the app legitimately needs user home directories.
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIXLimits socket types the process can create.Restrict to only what the app needs. Blocks raw packet sockets.
MemoryMax=2GSets a hard cgroup memory limit.Set to 80% of expected peak. Prevents OOM kills of critical system services.

These controls are declarative and version-controllable. If you are managing multiple servers, encode these patterns in Ansible roles or Terraform modules to ensure consistency. For deeper OS-level hardening context, review the Ubuntu security hardening guide before deploying.

Edit Unit File/etc/systemd/system/Validate Syntaxsystemd-analyze verifyReload Daemonsystemctl daemon-reloadRestart Servicesystemctl restart myappVerify Statussystemctl status + journalEnable on Bootsystemctl enable myapp
Safe deployment workflow: always validate syntax and reload the daemon before restarting to prevent configuration drift and downtime.

How do you manage logs and debug failures when running Python with systemd?

When you run Python in production with systemd, you should not write application logs to arbitrary files on disk. Instead, let systemd capture stdout and stderr via StandardOutput=journal. This integrates your Python app with journald, giving you structured metadata (timestamps, PIDs, unit names) without configuring log rotation manually. For applications using structured logging libraries like structlog or Python’s logging module, configure JSON output to stdout so journald captures parseable records. This approach feeds directly into tools discussed in the structured logging best practices guide.

Essential debugging commands

  1. Check live status: systemctl status myapp shows the last 10 log lines, exit codes, and uptime. If the service failed, the "Active" line will show failed (Result: exit-code).
  2. Query full logs: journalctl -u myapp --since "1 hour ago" --no-pager retrieves all entries for your unit. Add -o json-pretty for machine-readable output when piping to analysis tools.
  3. Follow real-time output: journalctl -u myapp -f tails the log stream during deployments or incident response.
  4. Inspect cgroup resources: systemctl status myapp includes memory/CPU usage if accounting is enabled. For deeper metrics, use systemd-cgtop to watch resource consumption across all services.
  5. Validate unit changes: Before reloading, run systemd-analyze verify /etc/systemd/system/myapp.service to catch syntax errors, missing dependencies, or invalid directives without risking downtime.

A frequent pitfall is assuming print() statements appear in logs immediately. Python buffers stdout when it detects a non-TTY environment. Set Environment=PYTHONUNBUFFERED=1 in your unit file or use the -u flag in ExecStart to force unbuffered output. Without this, your logs may lag minutes behind actual events during incidents.

How does systemd compare to Docker, supervisord, and bare process managers for Python?

Choosing the right process manager depends on your operational maturity and infrastructure constraints. While containers dominate new deployments, systemd remains the correct choice for bare-metal VMs, edge devices, and teams avoiding container orchestration overhead. Understanding these trade-offs prevents architectural regret.

systemd✓ Native OS Integration✓ Zero Overhead✓ Cgroup Resource Limits✓ Journal Logging✗ No Image Portability✗ Manual Dependency MgmtBest for: VMs, Edge,Compliance-Hardened HostsDocker / Podman✓ Portable Images✓ Reproducible Envs✓ Ecosystem Tooling✗ Runtime Overhead✗ Complex Networking✗ Log Driver Config NeededBest for: Microservices,CI/CD Parity, K8s Prepsupervisord✓ Simple Config✓ Legacy App Support✗ No Cgroup Isolation✗ Extra Process to Manage✗ Limited Security Sandbox✗ Manual Log RotationBest for: Legacy Systems,Non-systemd DistrosBare nohup/&✗ No Auto-Restart✗ No Logging✗ No Resource Limits✗ Not Audit-CompliantNever use in production
Decision matrix comparing process managers for Python: systemd offers native integration and security, while Docker provides portability at higher complexity.

In practice, I recommend systemd for any Python workload running directly on a Linux host where you control the OS. It provides cgroup-based resource limiting, socket activation, and watchdog integration that external supervisors cannot match without additional tooling. If your team already standardizes on containers, run your Python app inside Docker/Podman but still use systemd (via the container runtime’s unit file) to manage the container itself. Never use bare nohup or backgrounded processes in production; they lack supervision, logging, and audit trails required for compliance frameworks like ISO 27001 or SOC 2.

Deploy Reliable Python Services Today

To successfully run Python in production with systemd, treat your unit file as production code: version-control it, review it, and test it in staging before deployment. Start with the hardened template above, adapt the paths and resource limits to your workload, and validate every change with systemd-analyze verify. Pair this with structured logging and proper secret management to build services that survive real-world failures and pass security audits. If you need help designing compliant, observable Python infrastructure for your team, reach out to discuss your architecture.

Frequently Asked Questions

Create a unit file at /etc/systemd/system/myapp.service defining the ExecStart path to your Python binary and script. Set User, WorkingDirectory, and Restart=always directives, then run systemctl daemon-reload followed by systemctl enable --now myapp.service to activate it.

Systemd is built into modern Linux distributions, eliminating extra dependencies. It offers superior resource control via cgroups, integrated journal logging, socket activation, and automatic restart policies without requiring separate supervisor daemons or additional configuration management overhead.

Use the Environment= directive for single values or EnvironmentFile=/path/to/env for multiple variables. Never hardcode secrets directly in unit files; reference external env files with restricted permissions instead to maintain security and separation of concerns.

Point ExecStart to the virtual environment Python binary at /opt/myapp/venv/bin/python rather than modifying PATH. This ensures consistent dependency resolution and avoids conflicts with system Python packages during service startup and execution.

Use journalctl -u myapp.service -f for live streaming or add time ranges like --since today. Configure StandardOutput=journal in your unit file to capture stdout and stderr automatically without managing separate log rotation configurations.

Yes, set TimeoutStopSec=30 and send SIGTERM via KillSignal=SIGTERM. Your Python app must handle this signal properly using signal handlers or framework hooks to close database connections and finish processing before systemd sends SIGKILL.

Set Restart=on-failure and RestartSec=5 in your unit file. This restarts only on non-zero exit codes or signals, preventing restart loops from configuration errors while ensuring transient failures trigger automatic recovery within five seconds.

Always create a dedicated system user with nologin shell and minimal permissions. Specify User=myappuser and Group=myappgroup in the unit file to isolate the process and limit potential damage from application vulnerabilities or misconfigurations.

Use MemoryMax=512M, CPUQuota=80%, and LimitNOFILE=65535 directives in the Service section. These cgroup-based limits prevent runaway processes from consuming excessive resources and protect other services running on the same host.

Set WorkingDirectory to your application root where relative imports and config files resolve correctly. Avoid using home directories; prefer /opt/myapp or /srv/myapp with proper ownership matching the service user for predictable behavior.

Implement SIGHUP handling in your Python app to reload configuration or modules gracefully. Add ExecReload=/bin/kill -HUP $MAINPID to your unit file, enabling zero-downtime updates via systemctl reload myapp.service commands.

Yes, create a companion .socket unit with ListenStream=/run/myapp.sock and configure Accept=no. Systemd activates the Python service only when connections arrive, reducing idle resource consumption for low-traffic applications significantly.

Check journalctl -u myapp.service -n 50 --no-pager for error output. Test the ExecStart command manually as the service user, verify file permissions, and validate paths are absolute since systemd runs with minimal environment context.

Set TimeoutStartSec=90 if your app loads large models or warms caches. Default sixty-second timeouts often fail for ML workloads; adjust based on actual cold-start measurements to avoid premature termination during initialization phases.

Use Type=simple with a process manager like gunicorn inside the unit, or create template units [email protected] instantiated as [email protected] through [email protected]. Template approach gives individual process monitoring and independent restart capabilities per worker.