Flask Production Logging Setup
If you're trying to set up production logging for Flask or you cannot see useful logs after deployment, this guide shows you how to configure logging step-by-step. The goal is to capture Flask app logs, Gunicorn logs, and Nginx logs in predictable locations and verify that they are working.
Quick Fix / Quick Setup
Use journald for Gunicorn first, then confirm Nginx logs are active. This gives you a stable baseline before adding file-based Flask logging.
sudo mkdir -p /var/log/myflaskapp
sudo chown -R www-data:www-data /var/log/myflaskapp
cat <<'EOF' | sudo tee /etc/systemd/system/myflaskapp.service
[Unit]
Description=Gunicorn instance for myflaskapp
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/myflaskapp
Environment="PATH=/srv/myflaskapp/venv/bin"
ExecStart=/srv/myflaskapp/venv/bin/gunicorn \
--workers 3 \
--bind unix:/run/myflaskapp.sock \
--access-logfile - \
--error-logfile - \
--capture-output \
wsgi:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl restart myflaskapp
sudo journalctl -u myflaskapp -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/access.log
sudo tail -n 100 /var/log/nginx/error.log
This setup sends Gunicorn access and error output to the systemd journal, which is the fastest stable default on Ubuntu-based VPS deployments. Add structured Flask file logging after this baseline is working.
What’s Happening
In production, Flask logs often disappear because the development server logger is no longer the active process logger. A complete setup usually includes three layers: Flask application logs, Gunicorn runtime logs, and Nginx access/error logs. The goal is to make each layer visible, persistent, and easy to inspect during failures.
Step-by-Step Guide
- Choose a logging strategy
Use:journaldfor Gunicorn service output/var/log/nginx/*.logfor reverse proxy access and errors/var/log/myflaskapp/only if you need dedicated Flask file logs
Create the application log directory if needed:bashsudo mkdir -p /var/log/myflaskapp sudo chown -R www-data:www-data /var/log/myflaskapp sudo chmod 755 /var/log/myflaskapp - Configure Flask logging early in app startup
Initialize logging before serving requests so startup errors and request-time exceptions are captured.
Example for a single-file app:pythonimport logging import os from logging.handlers import RotatingFileHandler from flask import Flask app = Flask(__name__) log_dir = "/var/log/myflaskapp" os.makedirs(log_dir, exist_ok=True) formatter = logging.Formatter( "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" ) file_handler = RotatingFileHandler( f"{log_dir}/app.log", maxBytes=10_485_760, backupCount=5 ) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.INFO) stream_handler.setFormatter(formatter) app.logger.setLevel(logging.INFO) app.logger.addHandler(file_handler) app.logger.addHandler(stream_handler) app.logger.propagate = False - Avoid duplicate handlers in app factory setups
If you usecreate_app(), attach handlers once.pythonimport logging import os from logging.handlers import RotatingFileHandler from flask import Flask def create_app(): app = Flask(__name__) log_dir = "/var/log/myflaskapp" os.makedirs(log_dir, exist_ok=True) formatter = logging.Formatter( "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" ) file_handler = RotatingFileHandler( f"{log_dir}/app.log", maxBytes=10_485_760, backupCount=5 ) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.INFO) stream_handler.setFormatter(formatter) app.logger.setLevel(logging.INFO) if not app.logger.handlers: app.logger.addHandler(file_handler) app.logger.addHandler(stream_handler) app.logger.propagate = False return app - Log unhandled exceptions with context
Add an error handler so request path and method are included.pythonfrom flask import request @app.errorhandler(Exception) def handle_exception(e): app.logger.exception( "Unhandled exception on %s %s", request.method, request.path ) raise e - Configure Gunicorn to emit logs
The most reliable baseline is stdout/stderr intojournald.
If using command flags:bash/srv/myflaskapp/venv/bin/gunicorn \ --workers 3 \ --bind unix:/run/myflaskapp.sock \ --access-logfile - \ --error-logfile - \ --capture-output \ wsgi:app
If using a Gunicorn config file:pythonaccesslog = "-" errorlog = "-" capture_output = True loglevel = "info" - Update the systemd service
Define a production-safe service unit:ini[Unit] Description=Gunicorn instance for myflaskapp After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/srv/myflaskapp Environment="PATH=/srv/myflaskapp/venv/bin" ExecStart=/srv/myflaskapp/venv/bin/gunicorn \ --workers 3 \ --bind unix:/run/myflaskapp.sock \ --access-logfile - \ --error-logfile - \ --capture-output \ wsgi:app Restart=always RestartSec=3 [Install] WantedBy=multi-user.target
Reload and restart:bashsudo systemctl daemon-reload sudo systemctl restart myflaskapp sudo systemctl status myflaskapp - Enable Nginx access and error logging
Make sure the active server block writes to known files.nginxserver { listen 80; server_name example.com; access_log /var/log/nginx/myflaskapp_access.log; error_log /var/log/nginx/myflaskapp_error.log warn; location / { include proxy_params; proxy_pass http://unix:/run/myflaskapp.sock; } }
Validate and reload:bashsudo nginx -t sudo systemctl reload nginx - Check file permissions for Flask file logging
If Flask writes directly to/var/log/myflaskapp, the service user must own or be allowed to write there.bashsudo chown -R www-data:www-data /var/log/myflaskapp sudo chmod 755 /var/log/myflaskapp ls -lah /var/log/myflaskapp - Restart services in the correct order
Apply changes and validate:bashsudo systemctl restart myflaskapp sudo nginx -t sudo systemctl reload nginx - Trigger test log events
Add a temporary route:
@app.route("/log-test")
def log_test():
app.logger.info("Log test endpoint hit")
return "ok", 200
@app.route("/log-error")
def log_error():
raise RuntimeError("Intentional logging test error")
Then test:
curl -i http://127.0.0.1/log-test
curl -i http://127.0.0.1/log-error
- Inspect all three log layers
Gunicorn and service logs:
sudo journalctl -u myflaskapp -n 100 --no-pager
sudo journalctl -u myflaskapp -f
Nginx logs:
sudo tail -n 100 /var/log/nginx/access.log
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/myflaskapp_access.log
sudo tail -n 100 /var/log/nginx/myflaskapp_error.log
Flask file logs:
sudo tail -n 100 /var/log/myflaskapp/app.log
- Add log rotation for custom file logs
If you keep Flask file logs, rotate them.
/var/log/myflaskapp/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
Save as:
sudo nano /etc/logrotate.d/myflaskapp
Test it:
sudo logrotate -d /etc/logrotate.d/myflaskapp
- Standardize log content
Include:
- timestamp
- level
- module or logger name
- request path where relevant
- traceback for exceptions
Avoid:
- secrets
- tokens
- passwords
- full session contents
- environment variable dumps
- Use this as the default production baseline
Recommended baseline:
- Flask app logs to stream and optional rotating file
- Gunicorn logs to
journald - Nginx logs to
/var/log/nginx/ logrotatefor custom files- explicit testing after every deployment
Common Causes
- Gunicorn started without
--access-logfileor--error-logfile→ process logs are incomplete → enable both flags and restart the service. - Flask logger configured too late in startup → early exceptions never reach your handlers → initialize logging during app creation.
- Duplicate handlers attached on reload or app factory reuse → repeated log lines → guard with
if not app.logger.handlersand disable unwanted propagation. - Service user cannot write to
/var/log/<app>→ app log file stays empty or throws permission errors → fix ownership and permissions for the Gunicorn/systemd user. - Only Flask file logging is configured → Gunicorn startup and worker crashes are missed → inspect
journalctland enable--capture-output. - Nginx logs are being written to a different file than expected → proxy errors appear missing → verify
access_loganderror_login the active server block. - Log rotation is missing for custom files → logs grow until disk usage becomes a production issue → configure
logrotateor use journald retention. - Exceptions are swallowed by custom handlers → 500s happen without tracebacks → use
app.logger.exception(...)and re-raise or return a controlled logged response.
Debugging Section
Check service state:
sudo systemctl status myflaskapp
sudo systemctl cat myflaskapp
ps aux | grep gunicorn
Check Gunicorn and systemd logs:
sudo journalctl -u myflaskapp -n 200 --no-pager
sudo journalctl -u myflaskapp -f
Check Nginx configuration and logs:
sudo nginx -t
sudo tail -n 100 /var/log/nginx/access.log
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/myflaskapp_access.log
sudo tail -n 100 /var/log/nginx/myflaskapp_error.log
Check Flask file logs and permissions:
sudo tail -n 100 /var/log/myflaskapp/app.log
ls -lah /var/log/myflaskapp
ls -lah /run/myflaskapp.sock
What to look for:
- import errors during startup
Permission deniedon log directory or socket- missing
stdoutorstderrcapture - Nginx upstream connection errors
- 500 tracebacks present in Flask or Gunicorn logs but not both
- requests reaching Nginx but not reaching Gunicorn
If Gunicorn is failing before request handling, see Flask Gunicorn Service Failed to Start. If requests return 500, see Flask 500 Internal Server Error in Production.
Checklist
- Flask application logs are written to stdout, journald, or a dedicated file path
- Gunicorn runs with
--access-logfileand--error-logfileconfigured - systemd captures Gunicorn stdout/stderr without startup errors
- Nginx access and error logs are enabled and readable
- The service user can write to custom log directories if file logging is used
- Log rotation is configured for file-based logs
- Test requests generate visible info and error entries
- Tracebacks appear for unhandled exceptions
- No secrets or sensitive request data are written to logs
For a full deployment validation pass, use 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 500 Internal Server Error in Production
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: Should I log from Flask, Gunicorn, and Nginx together?
A: Yes. Each layer exposes different failures, so production debugging is much faster when all three are available.
Q: Is journald enough for a small Flask deployment?
A: Usually yes. It is often the simplest default for Gunicorn on Ubuntu with systemd.
Q: Why is app.logger.info not showing up?
A: The logger level may be too high, handlers may not be attached, or Gunicorn may not be capturing stdout/stderr.
Q: Should I enable Gunicorn access logs if Nginx already has them?
A: Usually optional. Many deployments keep Nginx access logs and rely on Gunicorn mainly for worker and error logs.
Q: How do I test logging safely?
A: Trigger a known endpoint that writes an info log and a controlled exception, then confirm entries in journalctl and Nginx logs.
Final Takeaway
Production Flask logging works best when each layer has a clear responsibility: Flask logs application events and exceptions, Gunicorn logs worker and runtime output, and Nginx logs requests and proxy failures. Start with journald plus Nginx logs, verify output end-to-end, then add structured file logging or centralized shipping after the baseline is stable.