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:
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
- 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.bashcd /var/www/myflask python3 -m venv venv source /var/www/myflask/venv/bin/activate pip install gunicorn - Confirm the Flask entrypoint works manually before using systemd.
If Gunicorn cannot import the app manually, systemd will also fail.bashcd /var/www/myflask /var/www/myflask/venv/bin/gunicorn --bind 127.0.0.1:8000 wsgi:app - Create the systemd unit file.bash
sudo nano /etc/systemd/system/myflask.service - 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 - Set
WorkingDirectorycorrectly.
This must point to the directory where your Flask app, package, orwsgi.pyfile lives.
Example:iniWorkingDirectory=/var/www/myflask - Set the Python environment path.
This ensures systemd uses the correct virtual environment.iniEnvironment="PATH=/var/www/myflask/venv/bin" - Set
ExecStartto the correct Gunicorn command.
The module target must match your project structure.iniExecStart=/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:iniExecStart=/var/www/myflask/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myproject.wsgi:app - Run the service as a non-root user.
Use a user that can read the application files and write any required sockets or logs.iniUser=www-data Group=www-data - Add restart behavior.
This lets systemd recover from transient process failures.iniRestart=always RestartSec=5 - Reload systemd after creating or editing the unit.
sudo systemctl daemon-reload
- Start the service and enable it on boot.
sudo systemctl enable --now myflask
- Check service status.
sudo systemctl status myflask --no-pager
Expected result: active (running).
- Inspect logs with journald.
This is the fastest way to find startup failures.
sudo journalctl -u myflask -f
- Test the local Gunicorn bind target.
For TCP:
curl http://127.0.0.1:8000
If you bind to a Unix socket, verify it exists:
ls -lah /run/myflask.sock
- 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).
- Reload and restart cleanly after unit changes.
sudo systemctl daemon-reload
sudo systemctl restart myflask
sudo systemctl status myflask --no-pager
- Optional: load secrets from an environment file.
Add this to the unit:
EnvironmentFile=/etc/myflask.env
Example file:
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
- Optional: use a Unix socket instead of a TCP port.
Replace the bind argument:
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.
- Optional: add resource controls.
TimeoutStartSec=30
TimeoutStopSec=30
LimitNOFILE=4096
Common Causes
- Wrong
ExecStartpath → systemd cannot find Gunicorn or the Python environment → pointExecStartto 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=orEnvironmentFile=in the unit. daemon-reloadnot 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:
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 statusshows whether the service is running, restarting, or exiting immediately.journalctl -u myflaskshows Python import errors, permission errors, missing env vars, and bad paths.systemd-analyze verifycatches unit syntax problems.ss -ltnp | grep 8000confirms Gunicorn is listening on the expected port.ls -lah /run/myflask.sockconfirms 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. -
WorkingDirectorypoints to the correct Flask project path. -
ExecStartpoints to the Gunicorn binary inside the virtual environment. - The Gunicorn app target such as
wsgi:appimports correctly. - The service runs as a non-root user with file access to the project.
-
systemctl daemon-reloadwas run after unit file changes. -
systemctl enable --now <service>completes without errors. -
systemctl status <service>showsactive (running). -
journalctl -u <service>does not show import, permission, or environment errors. - The local Gunicorn bind target responds to
curlor 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).
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Gunicorn Service Failed to Start
- 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.