Reference Guide Detailed deployment notes with production context and concrete examples.

systemd Basics for Flask Deployments

If you're trying to run Flask reliably in production or need Gunicorn to start automatically on boot, this guide shows you how to set up a systemd service step-by-step. The outcome is a managed Flask process that can start, stop, restart, and recover cleanly on a Linux server.

Quick Fix / Quick Setup

Use this minimal systemd unit to run Gunicorn as a persistent Flask service:

bash
sudo tee /etc/systemd/system/myflask.service >/dev/null <<'EOF'
[Unit]
Description=Gunicorn service for Flask app
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myflask
Environment="PATH=/var/www/myflask/venv/bin"
ExecStart=/var/www/myflask/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now myflask
sudo systemctl status myflask --no-pager
journalctl -u myflask -n 50 --no-pager

Replace /var/www/myflask, the service name, user/group, and wsgi:app with your actual project path and Flask entrypoint. This gives you a minimal production-ready systemd service for Gunicorn.

What’s Happening

  • systemd is the Linux service manager responsible for starting, stopping, and supervising long-running processes.
  • In a Flask deployment, systemd usually manages Gunicorn rather than the Flask development server.
  • A correct unit file ensures the app starts with the right user, working directory, Python environment, and restart policy.

Step-by-Step Guide

  1. Prepare the app directory and virtual environment.
    Confirm your project has a stable path and Gunicorn is installed in the same virtual environment used in production.
    bash
    cd /var/www/myflask
    python3 -m venv venv
    source /var/www/myflask/venv/bin/activate
    pip install gunicorn
    
  2. Confirm the Flask entrypoint works manually before using systemd.
    If Gunicorn cannot import the app manually, systemd will also fail.
    bash
    cd /var/www/myflask
    /var/www/myflask/venv/bin/gunicorn --bind 127.0.0.1:8000 wsgi:app
    
  3. Create the systemd unit file.
    bash
    sudo nano /etc/systemd/system/myflask.service
    
  4. Add a basic unit definition.
    Use this template:
    ini
    [Unit]
    Description=Gunicorn service for Flask app
    After=network.target
    
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/var/www/myflask
    Environment="PATH=/var/www/myflask/venv/bin"
    ExecStart=/var/www/myflask/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
    Restart=always
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target
    
  5. Set WorkingDirectory correctly.
    This must point to the directory where your Flask app, package, or wsgi.py file lives.
    Example:
    ini
    WorkingDirectory=/var/www/myflask
    
  6. Set the Python environment path.
    This ensures systemd uses the correct virtual environment.
    ini
    Environment="PATH=/var/www/myflask/venv/bin"
    
  7. Set ExecStart to the correct Gunicorn command.
    The module target must match your project structure.
    ini
    ExecStart=/var/www/myflask/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
    

    If your app is inside a package, use a package path instead:
    ini
    ExecStart=/var/www/myflask/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myproject.wsgi:app
    
  8. Run the service as a non-root user.
    Use a user that can read the application files and write any required sockets or logs.
    ini
    User=www-data
    Group=www-data
    
  9. Add restart behavior.
    This lets systemd recover from transient process failures.
    ini
    Restart=always
    RestartSec=5
    
  10. Reload systemd after creating or editing the unit.
bash
sudo systemctl daemon-reload
  1. Start the service and enable it on boot.
bash
sudo systemctl enable --now myflask
  1. Check service status.
bash
sudo systemctl status myflask --no-pager

Expected result: active (running).

  1. Inspect logs with journald.

This is the fastest way to find startup failures.

bash
sudo journalctl -u myflask -f
  1. Test the local Gunicorn bind target.

For TCP:

bash
curl http://127.0.0.1:8000

If you bind to a Unix socket, verify it exists:

bash
ls -lah /run/myflask.sock
  1. Update Nginx if you are proxying to Gunicorn.

Nginx must match the exact bind target used in ExecStart. For full reverse proxy setup, see Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide).

  1. Reload and restart cleanly after unit changes.
bash
sudo systemctl daemon-reload
sudo systemctl restart myflask
sudo systemctl status myflask --no-pager
  1. Optional: load secrets from an environment file.

Add this to the unit:

ini
EnvironmentFile=/etc/myflask.env

Example file:

bash
sudo tee /etc/myflask.env >/dev/null <<'EOF'
FLASK_ENV=production
SECRET_KEY=change-me
DATABASE_URL=postgresql://user:pass@localhost/dbname
EOF

sudo chmod 600 /etc/myflask.env
  1. Optional: use a Unix socket instead of a TCP port.

Replace the bind argument:

ini
ExecStart=/var/www/myflask/venv/bin/gunicorn --workers 3 --bind unix:/run/myflask.sock wsgi:app

Then confirm the socket path and permissions match what Nginx expects.

  1. Optional: add resource controls.
ini
TimeoutStartSec=30
TimeoutStopSec=30
LimitNOFILE=4096

Common Causes

  • Wrong ExecStart path → systemd cannot find Gunicorn or the Python environment → point ExecStart to the virtualenv binary.
  • Wrong app import target such as wsgi:app → Gunicorn starts then exits with import errors → verify the actual module and variable name.
  • Incorrect WorkingDirectory → relative imports or startup files cannot be found → set it to the project root.
  • Service runs as the wrong user → permission denied on project files, sockets, or logs → use a user with access to the app directory.
  • Environment variables not loaded → app fails on missing secrets or config values → use Environment= or EnvironmentFile= in the unit.
  • daemon-reload not run after edits → systemd still uses the old definition → reload and restart the service.
  • Port or socket mismatch with Nginx → Nginx cannot reach Gunicorn → make both sides use the same bind target.
  • Virtual environment missing dependencies → app import fails only in service mode → install packages in the same venv used by ExecStart.
  • SELinux or restrictive filesystem permissions → service starts but cannot access required files → inspect permissions and policy on hardened systems.
  • Service file syntax errors → systemd refuses to load or start the unit → validate with systemd-analyze verify.

Debugging Section

Check these commands in order:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now myflask
sudo systemctl restart myflask
sudo systemctl status myflask --no-pager -l
sudo journalctl -u myflask -n 100 --no-pager
sudo journalctl -u myflask -f
systemctl cat myflask
sudo systemd-analyze verify /etc/systemd/system/myflask.service
ss -ltnp | grep 8000
ls -lah /run/myflask.sock
curl -I http://127.0.0.1:8000
cd /var/www/myflask && /var/www/myflask/venv/bin/gunicorn --bind 127.0.0.1:8000 wsgi:app
namei -l /var/www/myflask
systemctl show myflask --property=Environment

What to look for:

  • systemctl status shows whether the service is running, restarting, or exiting immediately.
  • journalctl -u myflask shows Python import errors, permission errors, missing env vars, and bad paths.
  • systemd-analyze verify catches unit syntax problems.
  • ss -ltnp | grep 8000 confirms Gunicorn is listening on the expected port.
  • ls -lah /run/myflask.sock confirms socket creation and ownership.
  • Manual Gunicorn startup isolates application problems from unit-file problems.

If the service fails repeatedly, also check Flask Gunicorn Service Failed to Start.

Checklist

  • The unit file exists in /etc/systemd/system/ and uses the correct service name.
  • WorkingDirectory points to the correct Flask project path.
  • ExecStart points to the Gunicorn binary inside the virtual environment.
  • The Gunicorn app target such as wsgi:app imports correctly.
  • The service runs as a non-root user with file access to the project.
  • systemctl daemon-reload was run after unit file changes.
  • systemctl enable --now <service> completes without errors.
  • systemctl status <service> shows active (running).
  • journalctl -u <service> does not show import, permission, or environment errors.
  • The local Gunicorn bind target responds to curl or creates the expected socket file.
  • Nginx, if used, points to the same port or socket as Gunicorn.
  • The full deployment matches your production baseline in Flask Production Checklist (Everything You Must Do).

FAQ

Q: Should I use systemd for Flask in production?
A: Yes. systemd is the standard way to supervise Gunicorn, restart it on failure, and start it on boot.

Q: What process should systemd manage?
A: Usually Gunicorn, not the Flask development server.

Q: Why does systemctl start fail immediately?
A: Check journalctl -u <service> for import errors, bad paths, missing environment variables, or permission problems.

Q: Do I need daemon-reload every time I edit the unit file?
A: Yes. Run sudo systemctl daemon-reload before restarting the service.

Q: Can I use a Unix socket instead of a TCP port?
A: Yes. Use --bind unix:/run/<name>.sock and ensure Nginx and the service user have matching permissions.

Final Takeaway

systemd is the process supervisor layer that makes a Flask deployment persistent and manageable in production. Most service failures come from incorrect paths, users, environment variables, or app import targets. If Gunicorn starts manually and the unit file matches that exact command, the systemd setup is usually straightforward.