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

Flask Production Folder Structure Reference

If you're trying to organize a Flask app for production or fix a messy deployment layout, this guide shows you a proven folder structure step-by-step. The outcome is a predictable layout that works cleanly with Gunicorn, Nginx, systemd, static files, media uploads, logs, and repeatable deploys.

Quick Fix / Quick Setup

Create a production-safe base layout under /srv/myapp:

bash
sudo mkdir -p /srv/myapp/{app,run,shared/{static,media,logs},releases}
sudo python3 -m venv /srv/myapp/venv
sudo chown -R www-data:www-data /srv/myapp
find /srv/myapp -type d -exec chmod 755 {} \;

Reference tree:

text
/srv/myapp/
├── app/                 # current application code or symlink to active release
├── releases/            # timestamped releases for rollback
├── shared/
│   ├── static/          # collected static assets served by Nginx
│   ├── media/           # user uploads
│   └── logs/            # app and gunicorn logs if using file logging
├── run/                 # gunicorn socket or pid files
└── venv/                # Python virtual environment

Use /srv/myapp as the base path, keep code separate from writable data, and do not store uploads, sockets, or logs inside the app package directory.

What’s Happening

Production Flask deployments fail when code, writable data, sockets, and logs are mixed in one directory. Nginx, Gunicorn, and systemd require predictable paths and correct permissions. A stable layout prevents broken static paths, missing uploads, failed sockets, and destructive deploys.

Step-by-Step Guide

  1. Create one base directory
    Use a dedicated service path such as /srv/myapp instead of /root, /home, or an arbitrary working directory.
    bash
    sudo mkdir -p /srv/myapp
    
  2. Create the standard top-level directories
    Create directories for active code, releases, shared writable data, runtime files, and the virtualenv.
    bash
    sudo mkdir -p /srv/myapp/{app,run,shared/{static,media,logs},releases}
    sudo python3 -m venv /srv/myapp/venv
    
  3. Use a release-safe layout
    Either place code directly in /srv/myapp/app or use release directories with an active symlink.
    bash
    sudo mkdir -p /srv/myapp/releases/2026-04-21-120000
    sudo ln -sfn /srv/myapp/releases/2026-04-21-120000 /srv/myapp/app
    
  4. Keep the Flask package inside the app path
    Example layout:
    text
    /srv/myapp/app/
    ├── myproject/
    ├── wsgi.py
    ├── requirements.txt
    └── migrations/
    

    Your Gunicorn entrypoint should resolve from this directory, for example:
    bash
    /srv/myapp/venv/bin/gunicorn wsgi:app
    
  5. Store environment files outside the codebase
    Do not commit secrets into the repository or place them inside the Flask package.
    bash
    sudo touch /srv/myapp/shared/.env
    sudo chown www-data:www-data /srv/myapp/shared/.env
    sudo chmod 640 /srv/myapp/shared/.env
    
  6. Send static files to a shared path
    Keep static assets outside the repo so Nginx can serve them consistently across deploys.
    Example Flask config:
    python
    STATIC_FOLDER = "/srv/myapp/shared/static"
    

    If you use a build pipeline, output built assets there.
  7. Send uploads and media to a shared path
    User content must survive redeploys.
    Example Flask config:
    python
    UPLOAD_FOLDER = "/srv/myapp/shared/media"
    
  8. Store the Gunicorn socket in run/
    Do not place sockets in the repo or rely on /tmp.
    Example Gunicorn bind path:
    bash
    --bind unix:/srv/myapp/run/gunicorn.sock
    
  9. Use journald or a shared logs directory
    If file logging is required, keep logs outside release directories.
    bash
    sudo touch /srv/myapp/shared/logs/gunicorn.log
    sudo chown -R www-data:www-data /srv/myapp/shared/logs
    
  10. Set ownership for the runtime user
    If Gunicorn runs as www-data, assign access accordingly.
    bash
    sudo chown -R www-data:www-data /srv/myapp
    

    If deploys are performed by another user, split deploy ownership from runtime ownership and grant write access only where needed.
  11. Set safe directory permissions
    Keep directories traversable and writable only where required.
    bash
    find /srv/myapp -type d -exec chmod 755 {} \;
    sudo chmod 640 /srv/myapp/shared/.env
    sudo chmod 775 /srv/myapp/run /srv/myapp/shared/media /srv/myapp/shared/logs
    
  12. Configure systemd with consistent paths
    Example service:
    ini
    [Unit]
    Description=Gunicorn for myapp
    After=network.target
    
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/srv/myapp/app
    EnvironmentFile=/srv/myapp/shared/.env
    ExecStart=/srv/myapp/venv/bin/gunicorn \
      --workers 3 \
      --bind unix:/srv/myapp/run/gunicorn.sock \
      wsgi:app
    
    [Install]
    WantedBy=multi-user.target
    

    Then reload and start:
    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now myapp
    
  13. Configure Nginx to use shared static and media paths
    Example server block snippet:
    nginx
    server {
        listen 80;
        server_name example.com;
    
        location /static/ {
            alias /srv/myapp/shared/static/;
        }
    
        location /media/ {
            alias /srv/myapp/shared/media/;
        }
    
        location / {
            include proxy_params;
            proxy_pass http://unix:/srv/myapp/run/gunicorn.sock;
        }
    }
    

    Validate and reload:
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    
  14. Verify the active app symlink if using releases
    Confirm that /srv/myapp/app points to the expected release.
    bash
    readlink -f /srv/myapp/app
    
  15. Validate that shared data survives redeploys
    Replace the active release and confirm that shared/media, shared/static, shared/logs, run, and venv remain untouched.
  16. Use this recommended reference layout
    text
    /srv/myapp/
    ├── app/                  -> active codebase or symlink to current release
    ├── releases/             -> timestamped deployments for rollback
    ├── shared/
    │   ├── static/           -> static files served by Nginx
    │   ├── media/            -> user uploads persisted across deploys
    │   ├── logs/             -> optional file logs
    │   └── .env              -> environment file if used
    ├── run/                  -> Gunicorn socket and PID files
    └── venv/                 -> Python virtual environment
    

Common Causes

  • Code and uploads stored in the same directory → redeploys overwrite user data → move uploads to /srv/myapp/shared/media.
  • Gunicorn socket placed inside the repo or /tmp → Nginx cannot access it consistently or it disappears on reboot → use /srv/myapp/run/gunicorn.sock.
  • Static files stored only inside the Flask package → Nginx alias does not match deployment paths → serve from /srv/myapp/shared/static.
  • Using /root as the deployment base → systemd and Nginx hit permission barriers → move the app to /srv or another service-safe path.
  • Environment file committed into the application directory → secrets leak and config differs across releases → store secrets in /srv/myapp/shared/.env or systemd environment config.
  • Logs written inside release directories → logs disappear after each deploy → write to journald or /srv/myapp/shared/logs.
  • No releases/shared separation → rollback is hard and deploys are destructive → adopt releases plus app symlink plus shared data layout.

Debugging Section

Check structure, ownership, socket visibility, and service path consistency.

Inspect permissions and path traversal

bash
namei -l /srv/myapp
namei -l /srv/myapp/run
namei -l /srv/myapp/shared/media
namei -l /srv/myapp/run/gunicorn.sock

What to look for:

  • Every parent directory is traversable
  • Nginx and Gunicorn users can access the socket path
  • Writable directories are writable by the runtime user

List the directory tree with modes and owners

bash
ls -lah /srv/myapp
find /srv/myapp -maxdepth 3 -printf '%M %u %g %p\n'

What to look for:

  • run, shared/media, and optional shared/logs have write access for the service user
  • app points to the expected release if symlinks are used

Validate systemd service paths

bash
sudo systemctl cat myapp
sudo journalctl -u myapp -n 100 --no-pager

What to look for:

  • WorkingDirectory=/srv/myapp/app
  • ExecStart uses /srv/myapp/venv/bin/gunicorn
  • Socket path matches /srv/myapp/run/gunicorn.sock
  • Environment file path is correct

Check socket creation

bash
sudo ls -lah /srv/myapp/run
sudo ss -xl | grep gunicorn

What to look for:

  • gunicorn.sock exists
  • Gunicorn is actually listening on the Unix socket

Validate Nginx path mappings

bash
sudo nginx -t
sudo tail -n 100 /var/log/nginx/error.log

What to look for:

  • alias paths match /srv/myapp/shared/static/ and /srv/myapp/shared/media/
  • No permission denied errors
  • No bad socket path references

Confirm active release target

bash
readlink -f /srv/myapp/app

What to look for:

  • The symlink resolves to the intended release directory

Checklist

  • Application code is separate from writable directories.
  • Static files are outside the repo and mapped in Nginx.
  • Uploads/media are outside the repo and survive redeploys.
  • Gunicorn socket path exists under /srv/myapp/run.
  • systemd WorkingDirectory and ExecStart paths match the folder structure.
  • Runtime user has write access only where required: run, media, and optional logs.
  • Secrets are not stored inside the app package or Git repository.
  • Release-based deploys can switch the active app without moving shared data.

FAQ

Where should a Flask app live in production?

Use a dedicated service path such as /srv/myapp with separate directories for code, shared data, runtime files, and the virtualenv.

Should uploads be stored inside the Flask project folder?

No. Store uploads in a shared media directory outside the codebase so redeploys do not remove them.

Where should the Gunicorn socket go?

Place it in a runtime directory such as /srv/myapp/run/gunicorn.sock with permissions that allow both Gunicorn and Nginx to access it.

Should logs be stored in the app directory?

No. Use journald or a shared logs directory outside release paths.

Do I need a releases directory for small apps?

Not strictly, but it makes rollback and atomic deploys much safer.

Can I keep the virtualenv inside the project repo?

It is better to keep it at /srv/myapp/venv so code releases stay clean and replaceable.

Final Takeaway

A production-ready Flask folder structure separates code, shared data, runtime files, and environment configuration. That separation makes Nginx, Gunicorn, and systemd more predictable, keeps deploys replaceable, and prevents data loss during updates.