Deploy Code with systemd

Deploy code with Python and systemd: practical steps, common pitfalls, and next lessons in the DevOps automation path.

Focus: deploy code with python and systemd

Sponsored

You've written a solid Python script that automates a critical task, tested it locally, and pushed it to your repository. But the moment it needs to run continuously in production, you hit the classic DevOps wall: the script dies, nothing restarts it, and it never survives a reboot. Manually babysitting processes with nohup and & is a recipe for silent failures, and using a cron job that starts a new process every minute creates duplicates and race conditions. This lesson shows you how to deploy Python code with systemd, the standard init system on modern Linux distributions, turning your scripts into first-class, managed services that are resilient, observable, and production-ready.

The problem this lesson solves

Unmanaged Python scripts are a liability in any serious deployment. When you run a script with python app.py, it runs in your terminal's process group. Close the terminal, and the process gets a SIGHUP signal and dies. Even if you use nohup to ignore that signal, a crash in the script — an unhandled exception, an out-of-memory kill, or a segfault — leaves nothing behind but a dead process and an angry support ticket.

Cron jobs add their own set of problems. A cron entry that runs your script every minute will happily start a new instance even if the previous one is still running, leading to overlapping executions and corrupted state. Cron gives you no visibility into the script's health, no automatic restart policy, and no proper logging beyond mail-to-root. It's a scheduling tool, not a process manager.

What you need is a robust, declarative way to tell the OS: "Run this Python script, keep it alive, restart it if it fails, start it at boot, and capture its output." That's exactly what systemd provides.

Core concept / mental model

Think of systemd as the Linux operating system's factory floor supervisor. When your machine boots, systemd is the first process (PID 1) and is responsible for starting everything else. It reads unit files — declarative configuration files with [Unit], [Service], and [Install] sections — to learn how to manage a service. A unit file is like a work order: it specifies the command to run, the working directory, the user to run as, and the policy for restarts.

Your Python script, by contrast, is the worker. It doesn't need to know how to daemonize itself, write PID files, or handle logging — systemd takes care of those concerns. This separation of concerns is key: the unit file defines the deployment contract, and the Python code focuses on business logic.

Here's a word diagram of the flow:

System boot / systemctl start myapp
        |
        v
[systemd] --> reads /etc/systemd/system/myapp.service
        |
        |-- defines: ExecStart, Restart, Environment, User
        |
        v
[Python script] --> runs as a child of systemd
        |
        |-- logs to stdout/stderr
        |
        v
[systemd journal] --> captures logs, manages restarts

The key insight is that your script should run in the foreground (not daemonize itself) and write logs to stdout/stderr. systemd will handle backgrounding and log collection through the journal.

How it works step by step

Deploying a Python script with systemd involves a systematic sequence of steps. Each step builds on the previous one, so it's important to get the basics right.

  1. Write a production-ready Python script - The script should be executable, have a proper shebang (#!/usr/bin/env python3), and run in a loop if it's a long-running service. For a one-shot task, it can exit normally. - Use if __name__ == "__main__": to allow the script to be run directly or imported. - Make the script respect environment variables for configuration, so systemd can inject them via Environment=.

  2. Create a dedicated system user (optional but recommended) - Running services as root is bad practice. Create a service account with sudo useradd -r -s /usr/sbin/nologin myappuser so the service runs with minimal privileges.

  3. Place the script in a standard location - Install the script to an application directory like /opt/myapp/myapp.py and make it executable with chmod +x.

  4. Create the systemd unit file - The unit file tells systemd what to run, how to run it, and when to restart it. Start with a [Unit] section to describe the service and its dependencies, then [Service] with the execution details, and finally [Install] to enable start-at-boot.

  5. Reload systemd and enable the service - Run systemctl daemon-reload to make systemd aware of the new unit file, then systemctl enable myapp.service to create symlinks for boot start, and systemctl start myapp.service to launch it.

  6. Check status and logs - Use systemctl status myapp.service for a quick health check and journalctl -u myapp.service -f to follow logs and verify the script is behaving as expected.

  7. Verification and iteration - Test that the service survives a crash by killing its PID and watching systemd restart it. Check that it starts at boot by rebooting a test instance.

Hands-on walkthrough

Let's walk through a complete example. We'll create a simple Python web server using Flask that responds to requests and logs them.

1. The Python scriptmyapp.py:

#!/usr/bin/env python3
"""A minimal Flask web service designed to run under systemd."""
import os
import sys
from flask import Flask, jsonify

# Use an environment variable for port configuration (set in the unit file)
PORT = int(os.environ.get("MYAPP_PORT", "5000"))

app = Flask(__name__)

@app.route("/health")
def health():
    """Health check endpoint that returns 200 OK."""
    return jsonify({"status": "healthy"})

@app.route("/")
def index():
    """Return a simple greeting."""
    app.logger.info("Root endpoint hit")
    return jsonify({"service": "myapp", "port": PORT})

if __name__ == "__main__":
    # Run in foreground; systemd sends signals when it wants to stop us
    app.run(host="0.0.0.0", port=PORT)

2. Place it in the app directory and make it executable:

sudo mkdir -p /opt/myapp
sudo cp myapp.py /opt/myapp/myapp.py
sudo chmod +x /opt/myapp/myapp.py

3. Create the systemd unit file at /etc/systemd/system/myapp.service:

[Unit]
Description=My Python Flask Service
After=network.target

[Service]
User=myappuser
Group=myappuser
WorkingDirectory=/opt/myapp
Environment=MYAPP_PORT=8080
ExecStart=/usr/bin/python3 /opt/myapp/myapp.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

4. Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service

5. Verify it works:

sudo systemctl status myapp.service
curl http://localhost:8080/health
journalctl -u myapp.service -f

Expected output from status starts with:

● myapp.service - My Python Flask Service
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: enabled)
     Active: active (running) since ...
   Main PID: 12345 (python3)
     Memory: 15.2M

And the health check returns {"status": "healthy"}.

6. Test the restart policy:

Kill the main process and watch systemd revive it:

sudo kill $(systemctl show -p MainPID --value myapp.service)
sleep 6  # Wait for RestartSec
systemctl is-active myapp.service  # Should print 'active'

This proves the service restarts automatically under the on-failure policy.

Compare options / when to choose what

While systemd is the standard, it's not the only option. The table below compares systemd with other common approaches to running Python services.

Approach Best for Pros Cons
systemd Long-running services, boot-start, crash recovery Native to Linux, first-class dependency management, resource controls, no extra install Linux only, steeper learning curve
nohup + & Quick, informal dev sessions Minimal commands No auto-restart, no boot start, process not managed, logs to file
cron Periodic/scheduled scripts (e.g., daily backups) Simple scheduling, minimal setup No overlap protection, no health checking, no graceful shutdown
Supervisor Environments not using systemd (e.g., older containers) Cross-distro, easy config, web console Extra process to manage, not integrated with OS boot
Docker + restart policies Containerized microservices Isolation, reproducible, restart: unless-stopped Requires container runtime, adds orchestration overhead

When to choose systemd: - Your deployment target is a bare-metal or virtual machine running a modern Linux distribution. - You need the service to start at boot and auto-restart on failure. - You want tight integration with the OS ecosystem (users, permissions, resource limits).

When to look elsewhere: - If you're deploying to containerized environments (Kubernetes, Docker Swarm), obviously the container orchestrator takes over this role. - For legacy systems without systemd (e.g., older SysV init), consider supervisor or upstart.

Troubleshooting & edge cases

Even with a clean setup, things can go wrong. Here are the most common pitfalls and how to fix them.

  • Service starts then immediately exits with code 1 — Your Python script likely threw an exception. Check the logs with journalctl -u myapp.service -e. If the error is a missing module, ensure the Python environment (e.g., a virtualenv) is correctly specified in ExecStart. For a virtualenv, set ExecStart=/opt/myapp/venv/bin/python /opt/myapp/myapp.py.

  • Permission denied even though the script is executable — The service runs under the user specified by User=. That user must have read and execute permissions on the script and its directory. If you plan to write files, the WorkingDirectory and any output paths must be writable by that user.

  • The service doesn't start at boot — You probably forgot to run systemctl enable. Enabling creates the WantedBy symlinks. Another cause is the After=network.target dependency not being met if network is not ready; tune with After=network-online.target and Wants=network-online.target.

  • Overlapping runs of a one-shot script — If you need a periodic task that must not overlap with a previous run, add a lock file. systemd doesn't manage this natively. Use a file lock in Python or use systemd.timer (a cron-like unit) which can be configured to avoid overlaps with Persistent=true.

  • Environment variables not visible to the script — The Environment= line in the unit file only sets variables. They are not exported to child processes unless you start your script via a shell. If your script (or its child processes) needs them, use EnvironmentFile= to load from a file, or invoke the script via /bin/sh -c 'set -a; source /etc/default/myapp; exec python myapp.py' — though better to simply let your Python script parse a config file.

  • systemctl status shows activating (auto-restart) repeatedly — This means the service is failing too quickly. Check logs; often it's a port already in use. Use RestartSec to add a delay and StartLimitIntervalSec/StartLimitBurst to limit retries.

  • The script doesn't read stdin — Not a problem for services, but if your script expects input, systemd by default has no stdin. Pass configuration via environment variables or a config file, not interactive input.

What you learned & what's next

You've learned the core concept: deploying Python code with systemd means declaring a service contract in a unit file that the OS manages on your behalf. You now can:

  • Write a Python script that runs in the foreground and logs to stdout.
  • Create a systemd unit file with proper [Unit], [Service], and [Install] sections.
  • Start, enable, and monitor the service using systemctl and journalctl.
  • Configure auto-restart with Restart=on-failure.
  • Verify that the service survives crashes and starts at boot.

These skills form the foundation of robust deployments in any Linux environment. Your next step in this Python for DevOps automation track is to explore how to automate the deployment process itself — perhaps using Ansible to push unit files and scripts to many hosts, or diving into containerization with Docker later on. Each lesson builds on the last, so keep practicing by creating your own unit files for different kinds of scripts.

Now, go deploy something that stays up!

Practice recap

Create a simple Python script that writes a timestamp to a file every 5 seconds, then package it as a systemd service with a Restart=always policy. Enable and start it, then kill the process and verify it restarts. After that, check the journal logs to see the output. This will cement the workflow of unit file creation, service control, and troubleshooting.

Common mistakes

  • Using python instead of python3 in ExecStart, leading to 'command not found' or the wrong interpreter, especially when the system Python is 2.x.
  • Forgetting to enable the service (systemctl enable) and then wondering why it doesn't start at boot.
  • Running the service as root when a dedicated non-privileged user would be safer; this increases the risk of a compromised service having full system access.
  • Not setting WorkingDirectory and using relative paths in the script, causing 'FileNotFoundError' when the working directory is systemd's default root.
  • Assuming the service will restart on any exit code, but Restart=on-failure does not restart on clean exit (code 0). Use Restart=always if you want unconditional restarts.

Variations

  1. Use a virtualenv in ExecStart to isolate dependencies: ExecStart=/opt/myapp/venv/bin/python /opt/myapp/myapp.py.
  2. For one-shot scripts, use Type=oneshot and RemainAfterExit=yes to represent a task that runs and is considered 'active'.
  3. Use systemd.timer units to schedule periodic Python scripts with built-in overlap protection and persistent missed-run semantics.

Real-world use cases

  • Running a Flask or Django web app as a robust production service that auto-restarts after crashes and starts on boot.
  • Executing a long-running data ingestion daemon (e.g., polling an API) that must stay alive 24/7 and log to the journal.
  • Scheduling a daily backup script via systemd timers that prevents overlapping runs and survives reboots without extra tooling.

Key takeaways

  • systemd turns a Python script into a managed, restartable, boot-persistent service via unit files.
  • Scripts should run in the foreground and log to stdout/stderr for systemd to capture.
  • Key directives: ExecStart, Restart, RestartSec, Environment, and Under for security.
  • Always run systemctl daemon-reload after editing a unit file and systemctl enable to start at boot.
  • Debugging is easier with journalctl -u <service> and systemctl status.
  • Choose systemd for bare-metal/VMs; for containers, use orchestrator-native restart policies.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.