A Python application: gunicorn and systemd
Running Django, Flask, or FastAPI on VDS: virtual environment, gunicorn with a socket, systemd service, and choosing the number of workers for your tier.
更新于 2026年8月23日
The built-in development server (manage.py runserver, flask run) isn't designed for production load. On a VPS, you install gunicorn or uvicorn, and a web server serves them externally.
Environment
Never install project packages into the system Python — a system update will break the application.
apt install -y python3 python3-venv python3-pip
useradd -r -m -d /srv/app -s /usr/sbin/nologin appuser
sudo -u appuser python3 -m venv /srv/app/venv
sudo -u appuser /srv/app/venv/bin/pip install -r /srv/app/requirements.txt
sudo -u appuser /srv/app/venv/bin/pip install gunicorn
For FastAPI, additionally install uvicorn[standard] — gunicorn runs it as a worker class.
Service
/etc/systemd/system/myapp.service:
[Unit]
Description=Python app
After=network-online.target postgresql.service
[Service]
User=appuser
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
ExecStart=/srv/app/venv/bin/gunicorn \
--workers 3 \
--bind unix:/run/myapp/gunicorn.sock \
--access-logfile - \
myproject.wsgi:application
RuntimeDirectory=myapp
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
For FastAPI, replace the launch line with --worker-class uvicorn.workers.UvicornWorker app.main:app.
systemctl daemon-reload
systemctl enable --now myapp
journalctl -u myapp -f
RuntimeDirectory=myapp creates /run/myapp with the required permissions on every start — otherwise there's nowhere to put the socket.
How many workers
The usual formula is 2 × cores + 1, but it runs into memory limits: each Django worker uses 80–200 MB.
| Tier | Cores | Workers |
|---|---|---|
| VDS-1 | 1 | 2 |
| VDS-2 | 2 | 3 |
| VDS-3 | 4 | 5 |
| VDS-4 | 6 | 9 |
If the application spends a lot of time waiting on the network and database, add --threads 2 instead of increasing the number of processes.
Web server
location / {
proxy_pass http://unix:/run/myapp/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ { alias /srv/app/static/; }
Serve Django static files directly with Nginx after collecting them: collectstatic. Passing them through Python adds unnecessary load.
Common errors
| Symptom | Cause |
|---|---|
502 and connect() to unix:... failed (13) | Nginx doesn't have permissions for the socket — set --umask 007 and a shared group |
ModuleNotFoundError | Running with the system Python instead of from venv |
DisallowedHost in Django | The domain isn't added to ALLOWED_HOSTS |
| Workers are killed by timeout | A long request — increase --timeout, move heavy work to a queue |
| Everything stopped after a release | You forgot systemctl restart myapp — code in memory doesn't update itself |
Keep secrets in an
EnvironmentFilewith600permissions, not insettings.py. A settings file will end up in the repository sooner or later.