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:
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:
/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
- Create one base directory
Use a dedicated service path such as/srv/myappinstead of/root,/home, or an arbitrary working directory.bashsudo mkdir -p /srv/myapp - Create the standard top-level directories
Create directories for active code, releases, shared writable data, runtime files, and the virtualenv.bashsudo mkdir -p /srv/myapp/{app,run,shared/{static,media,logs},releases} sudo python3 -m venv /srv/myapp/venv - Use a release-safe layout
Either place code directly in/srv/myapp/appor use release directories with an active symlink.bashsudo mkdir -p /srv/myapp/releases/2026-04-21-120000 sudo ln -sfn /srv/myapp/releases/2026-04-21-120000 /srv/myapp/app - 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 - Store environment files outside the codebase
Do not commit secrets into the repository or place them inside the Flask package.bashsudo touch /srv/myapp/shared/.env sudo chown www-data:www-data /srv/myapp/shared/.env sudo chmod 640 /srv/myapp/shared/.env - Send static files to a shared path
Keep static assets outside the repo so Nginx can serve them consistently across deploys.
Example Flask config:pythonSTATIC_FOLDER = "/srv/myapp/shared/static"
If you use a build pipeline, output built assets there. - Send uploads and media to a shared path
User content must survive redeploys.
Example Flask config:pythonUPLOAD_FOLDER = "/srv/myapp/shared/media" - 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 - Use journald or a shared logs directory
If file logging is required, keep logs outside release directories.bashsudo touch /srv/myapp/shared/logs/gunicorn.log sudo chown -R www-data:www-data /srv/myapp/shared/logs - Set ownership for the runtime user
If Gunicorn runs aswww-data, assign access accordingly.bashsudo 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. - Set safe directory permissions
Keep directories traversable and writable only where required.bashfind /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 - 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:bashsudo systemctl daemon-reload sudo systemctl enable --now myapp - Configure Nginx to use shared static and media paths
Example server block snippet:nginxserver { 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:bashsudo nginx -t sudo systemctl reload nginx - Verify the active app symlink if using releases
Confirm that/srv/myapp/apppoints to the expected release.bashreadlink -f /srv/myapp/app - Validate that shared data survives redeploys
Replace the active release and confirm thatshared/media,shared/static,shared/logs,run, andvenvremain untouched. - Use this recommended reference layouttext
/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
/rootas the deployment base → systemd and Nginx hit permission barriers → move the app to/srvor another service-safe path. - Environment file committed into the application directory → secrets leak and config differs across releases → store secrets in
/srv/myapp/shared/.envor 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
releasesplusappsymlink plusshareddata layout.
Debugging Section
Check structure, ownership, socket visibility, and service path consistency.
Inspect permissions and path traversal
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
ls -lah /srv/myapp
find /srv/myapp -maxdepth 3 -printf '%M %u %g %p\n'
What to look for:
run,shared/media, and optionalshared/logshave write access for the service userapppoints to the expected release if symlinks are used
Validate systemd service paths
sudo systemctl cat myapp
sudo journalctl -u myapp -n 100 --no-pager
What to look for:
WorkingDirectory=/srv/myapp/appExecStartuses/srv/myapp/venv/bin/gunicorn- Socket path matches
/srv/myapp/run/gunicorn.sock - Environment file path is correct
Check socket creation
sudo ls -lah /srv/myapp/run
sudo ss -xl | grep gunicorn
What to look for:
gunicorn.sockexists- Gunicorn is actually listening on the Unix socket
Validate Nginx path mappings
sudo nginx -t
sudo tail -n 100 /var/log/nginx/error.log
What to look for:
aliaspaths match/srv/myapp/shared/static/and/srv/myapp/shared/media/- No permission denied errors
- No bad socket path references
Confirm active release target
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
WorkingDirectoryandExecStartpaths match the folder structure. - Runtime user has write access only where required:
run,media, and optionallogs. - Secrets are not stored inside the app package or Git repository.
- Release-based deploys can switch the active app without moving shared data.
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Static and Media Files Production Setup
- Flask Production Checklist (Everything You Must Do)
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.