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

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.

bash
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

  1. Choose a logging strategy
    Use:
    • journald for Gunicorn service output
    • /var/log/nginx/*.log for reverse proxy access and errors
    • /var/log/myflaskapp/ only if you need dedicated Flask file logs

    Create the application log directory if needed:
    bash
    sudo mkdir -p /var/log/myflaskapp
    sudo chown -R www-data:www-data /var/log/myflaskapp
    sudo chmod 755 /var/log/myflaskapp
    
  2. 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:
    python
    import 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
    
  3. Avoid duplicate handlers in app factory setups
    If you use create_app(), attach handlers once.
    python
    import 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
    
  4. Log unhandled exceptions with context
    Add an error handler so request path and method are included.
    python
    from flask import request
    
    @app.errorhandler(Exception)
    def handle_exception(e):
        app.logger.exception(
            "Unhandled exception on %s %s",
            request.method,
            request.path
        )
        raise e
    
  5. Configure Gunicorn to emit logs
    The most reliable baseline is stdout/stderr into journald.
    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:
    python
    accesslog = "-"
    errorlog = "-"
    capture_output = True
    loglevel = "info"
    
  6. 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:
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart myflaskapp
    sudo systemctl status myflaskapp
    
  7. Enable Nginx access and error logging
    Make sure the active server block writes to known files.
    nginx
    server {
        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:
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    
  8. 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.
    bash
    sudo chown -R www-data:www-data /var/log/myflaskapp
    sudo chmod 755 /var/log/myflaskapp
    ls -lah /var/log/myflaskapp
    
  9. Restart services in the correct order
    Apply changes and validate:
    bash
    sudo systemctl restart myflaskapp
    sudo nginx -t
    sudo systemctl reload nginx
    
  10. Trigger test log events

Add a temporary route:

python
@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:

bash
curl -i http://127.0.0.1/log-test
curl -i http://127.0.0.1/log-error
  1. Inspect all three log layers

Gunicorn and service logs:

bash
sudo journalctl -u myflaskapp -n 100 --no-pager
sudo journalctl -u myflaskapp -f

Nginx logs:

bash
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:

bash
sudo tail -n 100 /var/log/myflaskapp/app.log
  1. Add log rotation for custom file logs

If you keep Flask file logs, rotate them.

conf
/var/log/myflaskapp/*.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    copytruncate
}

Save as:

bash
sudo nano /etc/logrotate.d/myflaskapp

Test it:

bash
sudo logrotate -d /etc/logrotate.d/myflaskapp
  1. 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
  1. 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/
  • logrotate for custom files
  • explicit testing after every deployment

Common Causes

  • Gunicorn started without --access-logfile or --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.handlers and 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 journalctl and enable --capture-output.
  • Nginx logs are being written to a different file than expected → proxy errors appear missing → verify access_log and error_log in the active server block.
  • Log rotation is missing for custom files → logs grow until disk usage becomes a production issue → configure logrotate or 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:

bash
sudo systemctl status myflaskapp
sudo systemctl cat myflaskapp
ps aux | grep gunicorn

Check Gunicorn and systemd logs:

bash
sudo journalctl -u myflaskapp -n 200 --no-pager
sudo journalctl -u myflaskapp -f

Check Nginx configuration and logs:

bash
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:

bash
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 denied on log directory or socket
  • missing stdout or stderr capture
  • 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-logfile and --error-logfile configured
  • 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).

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.