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

Flask systemd + Gunicorn Service Setup

If you're trying to run a Flask app under Gunicorn as a managed Linux service, this guide shows you how to set up systemd step-by-step. The goal is a reliable service that starts on boot, restarts on failure, and can be controlled with standard systemctl commands.

Quick Fix / Quick Setup

Create a basic systemd unit, reload systemd, start the service, and verify the socket:

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=/srv/myflask
Environment="PATH=/srv/myflask/venv/bin"
ExecStart=/srv/myflask/venv/bin/gunicorn --workers 3 --bind unix:/run/myflask.sock wsgi:app
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now myflask
sudo systemctl status myflask --no-pager
ls -l /run/myflask.sock

Replace /srv/myflask, the virtualenv path, service name, socket path, and wsgi:app with your actual project values. Use a TCP bind like 127.0.0.1:8000 instead of a Unix socket if that matches your Nginx setup.

What’s Happening

  • systemd runs Gunicorn as a managed background service.
  • Gunicorn serves the Flask WSGI app and listens on a Unix socket or local TCP port.
  • systemd handles startup order, restarts, logs, and boot-time enablement.
  • Most failures come from incorrect paths, wrong WSGI entrypoint, missing virtualenv dependencies, or socket permission mismatches.

Step-by-Step Guide

  1. Verify the Flask app runs manually
    Confirm the app works before introducing systemd:
    bash
    source /srv/myflask/venv/bin/activate
    cd /srv/myflask
    gunicorn --bind 127.0.0.1:8000 wsgi:app
    

    If this fails, fix the app import or dependencies first.
  2. Create or verify the WSGI entrypoint
    Example wsgi.py:
    python
    from app import app
    

    Your Gunicorn target must match this file and object:
    bash
    wsgi:app
    
  3. Choose a bind method
    Use one of these:
    Unix socket:
    bash
    unix:/run/myflask.sock
    

    TCP:
    bash
    127.0.0.1:8000
    

    Match this exactly in your Nginx upstream configuration.
  4. Create the systemd unit file
    bash
    sudo nano /etc/systemd/system/myflask.service
    
  5. Add the unit configuration
    Recommended example:
    ini
    [Unit]
    Description=Gunicorn service for Flask app
    After=network.target
    
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/srv/myflask
    Environment="PATH=/srv/myflask/venv/bin"
    Environment="FLASK_ENV=production"
    ExecStart=/srv/myflask/venv/bin/gunicorn --workers 3 --bind unix:/run/myflask.sock wsgi:app
    Restart=always
    RestartSec=3
    
    [Install]
    WantedBy=multi-user.target
    
  6. Add environment variables if needed
    If your app depends on secrets or database settings, use inline Environment= values or an environment file.
    Example environment file:
    bash
    sudo tee /etc/myflask.env > /dev/null <<'EOF'
    SECRET_KEY=change-me
    DATABASE_URL=postgresql://user:pass@localhost/dbname
    EOF
    

    Then update the service:
    ini
    [Service]
    EnvironmentFile=/etc/myflask.env
    
  7. Reload systemd
    bash
    sudo systemctl daemon-reload
    
  8. Start the service
    bash
    sudo systemctl start myflask
    
  9. Enable the service at boot
    bash
    sudo systemctl enable myflask
    
  10. Verify the service state
bash
sudo systemctl status myflask --no-pager
ps aux | grep gunicorn
  1. Verify the listener
    For TCP:
bash
ss -ltnp | grep 8000

For Unix socket:

bash
ls -l /run/myflask.sock
  1. Confirm Nginx points to the same upstream
    Example Nginx upstream for Unix socket:
nginx
location / {
    proxy_pass http://unix:/run/myflask.sock;
    include proxy_params;
}

Example Nginx upstream for TCP:

nginx
location / {
    proxy_pass http://127.0.0.1:8000;
    include proxy_params;
}
  1. Test restart behavior
bash
sudo systemctl restart myflask
sudo systemctl status myflask --no-pager
  1. Review logs for startup and import errors
bash
sudo journalctl -u myflask -n 100 --no-pager
  1. Apply updates safely after unit changes
    If you edit the service file again:
bash
sudo systemctl daemon-reload
sudo systemctl restart myflask

For full reverse proxy setup, see Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide).

Common Causes

  • Wrong ExecStart path → systemd cannot find Gunicorn → use the full virtualenv binary path such as:
    bash
    /srv/myflask/venv/bin/gunicorn
    
  • Incorrect WSGI target → Gunicorn cannot import the app → verify the module and object, for example:
    bash
    wsgi:app
    
  • Wrong WorkingDirectory → relative imports or file paths fail → point WorkingDirectory to the project root.
  • Environment variables missing under systemd → app starts manually but fails as a service → add Environment= lines or EnvironmentFile=.
  • Socket path mismatch with Nginx → service runs but Nginx returns 502 → make the Gunicorn bind path match the Nginx upstream exactly. See Fix Flask 502 Bad Gateway (Step-by-Step Guide).
  • Permission issue on Unix socket or app files → Nginx or Gunicorn cannot access required paths → align User, Group, and file permissions.
  • Virtualenv dependencies not installed → worker boot fails with ModuleNotFoundError → install dependencies inside the same virtualenv used by ExecStart.
  • Service edited without daemon-reload → changes do not apply → run:
    bash
    sudo systemctl daemon-reload
    

Debugging Section

Check service status:

bash
sudo systemctl status myflask --no-pager

Read recent logs:

bash
sudo journalctl -u myflask -n 100 --no-pager

Follow logs live:

bash
sudo journalctl -u myflask -f

Show the loaded service file:

bash
sudo systemctl cat myflask

Reload and restart after changes:

bash
sudo systemctl daemon-reload
sudo systemctl restart myflask

Check running Gunicorn processes:

bash
ps aux | grep gunicorn

Check Unix socket:

bash
ls -l /run/myflask.sock

Check TCP listener:

bash
ss -ltnp | grep 8000

Test as the service user:

bash
sudo -u www-data /srv/myflask/venv/bin/gunicorn --bind 127.0.0.1:8000 wsgi:app

Test Python import directly:

bash
cd /srv/myflask && /srv/myflask/venv/bin/python -c "import wsgi; print(wsgi.app)"

Validate Nginx config:

bash
sudo nginx -t

What to look for:

  • ModuleNotFoundError or import errors
  • Permission denied on app files or socket paths
  • wrong WorkingDirectory
  • missing environment variables
  • socket or port mismatch between Gunicorn and Nginx
  • immediate restart loops in systemd

If the unit refuses to start, see Flask Gunicorn Service Failed to Start. For systemd behavior and unit syntax, see systemd Basics for Flask Deployments.

Checklist

  • Service file is saved in /etc/systemd/system/
  • WorkingDirectory points to the project root
  • ExecStart uses the full path to the Gunicorn binary in the virtualenv
  • WSGI module path matches the real app entrypoint
  • User and Group can read the app files
  • Socket or TCP bind matches the Nginx upstream
  • systemd daemon was reloaded after unit changes
  • Service is enabled and running
  • journalctl shows no import or permission errors

Before go-live, validate against Flask Production Checklist (Everything You Must Do).

FAQ

Q: Should I run Gunicorn as root?
A: No. Run it as a non-root service user such as www-data or a dedicated app user.

Q: Where should the service file go?
A: Use /etc/systemd/system/<service>.service for a custom system service.

Q: How do I apply changes to the service file?
A: Run sudo systemctl daemon-reload and then restart the service.

Q: What is the correct ExecStart format?
A: Use the full Gunicorn binary path, bind option, and WSGI target, for example:

bash
/path/to/venv/bin/gunicorn --bind unix:/run/app.sock wsgi:app

Q: Should Gunicorn bind to a socket or a TCP port?
A: Either works. Unix sockets are common with local Nginx reverse proxy setups; TCP is simpler to debug.

Q: Why use systemd instead of running Gunicorn manually?
A: systemd handles boot startup, restarts, logging, and service management.

Q: Why does the service work manually but fail under systemd?
A: systemd uses a different environment, working directory, and PATH than your interactive shell.

Q: Do I need daemon-reload after editing the unit?
A: Yes. Run sudo systemctl daemon-reload before restarting the service.

Q: Why does Nginx still return 502 after the service starts?
A: Usually the upstream in Nginx does not match the Gunicorn socket or port, or the socket permissions are wrong.

Final Takeaway

A correct systemd unit makes Gunicorn predictable in production. Most setup failures are caused by wrong paths, wrong app import target, or mismatched socket settings. Validate the service manually first, then lock in the unit file, enable it, and confirm logs are clean.