Flask Environment Variables and Secrets Setup
If you're trying to configure Flask environment variables and secrets for production, this guide shows you how to set them up safely and load them reliably with systemd and Gunicorn. The goal is to keep secrets out of source code and ensure your app reads the correct production settings on every deploy.
Quick Fix / Quick Setup
Create a protected environment file, point your systemd service at it, then restart Gunicorn.
sudo mkdir -p /etc/myflaskapp
sudo tee /etc/myflaskapp/myflaskapp.env > /dev/null <<'EOF'
FLASK_ENV=production
SECRET_KEY=replace-with-long-random-value
DATABASE_URL=postgresql://user:password@127.0.0.1:5432/dbname
EOF
sudo chmod 600 /etc/myflaskapp/myflaskapp.env
sudo sed -i '/^EnvironmentFile=/d' /etc/systemd/system/myflaskapp.service
sudo tee /etc/systemd/system/myflaskapp.service > /dev/null <<'EOF'
[Unit]
Description=Gunicorn for myflaskapp
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myflaskapp
EnvironmentFile=/etc/myflaskapp/myflaskapp.env
ExecStart=/var/www/myflaskapp/venv/bin/gunicorn -w 3 -b unix:/run/myflaskapp.sock wsgi:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl restart myflaskapp
sudo systemctl status myflaskapp --no-pager
sudo systemctl show myflaskapp --property=Environment
Use an EnvironmentFile for production instead of exporting variables manually in a shell. Store the file outside the project directory, restrict permissions, and restart the service after changes.
What’s Happening
Flask reads configuration from process environment variables at startup. In production, Gunicorn usually runs under systemd, so variables exported in your interactive shell are not automatically available to the service. Secrets fail to load when they are defined in the wrong place, loaded too late, or blocked by file permission or service configuration issues.
Step-by-Step Guide
- Decide which values must come from environment variables.
Typical production values:SECRET_KEYDATABASE_URL- mail credentials
- API tokens
- Flask config mode
- feature flags
- Remove hardcoded secrets from Flask config.
Read values withos.environoros.getenv.pythonfrom os import environ SECRET_KEY = environ["SECRET_KEY"] SQLALCHEMY_DATABASE_URI = environ["DATABASE_URL"]
If missing values should fail fast, useenviron[...]. If optional, usegetenv. - Create a dedicated environment file outside the repository.bash
sudo mkdir -p /etc/myflaskapp sudo nano /etc/myflaskapp/myflaskapp.env
Add:dotenvFLASK_ENV=production SECRET_KEY=replace-with-long-random-value DATABASE_URL=postgresql://user:password@127.0.0.1:5432/dbname - Restrict permissions on the secrets file.bash
sudo chown root:root /etc/myflaskapp/myflaskapp.env sudo chmod 600 /etc/myflaskapp/myflaskapp.env - Configure systemd to load the environment file.
Edit/etc/systemd/system/myflaskapp.service:ini[Unit] Description=Gunicorn for myflaskapp After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/var/www/myflaskapp EnvironmentFile=/etc/myflaskapp/myflaskapp.env ExecStart=/var/www/myflaskapp/venv/bin/gunicorn -w 3 -b unix:/run/myflaskapp.sock wsgi:app Restart=always [Install] WantedBy=multi-user.target
Ensure:WorkingDirectorymatches your app pathExecStartpoints to the correct virtualenv- the socket or bind target matches your Nginx config
- Reload systemd and restart the service.bash
sudo systemctl daemon-reload sudo systemctl restart myflaskapp sudo systemctl status myflaskapp --no-pager - Verify the running service sees the variables.bash
sudo systemctl show myflaskapp --property=Environment sudo cat /proc/$(pgrep -f 'gunicorn.*myflaskapp' | head -n1)/environ | tr '\0' '\n' | grep -E 'FLASK_ENV|DATABASE_URL|SECRET_KEY' - If you use
python-dotenv, keep it for development only unless explicitly needed in production.
For local development,.envcan be fine. For production, prefer systemdEnvironmentFileor a dedicated secret manager. Do not assume Gunicorn will read.envautomatically. - Load config before initializing extensions.
If you use an application factory, ensure environment-backed config is available before initializing extensions.
Example order:pythondef create_app(): app = Flask(__name__) app.config.from_object("config.ProductionConfig") db.init_app(app) mail.init_app(app) cache.init_app(app) return app - Rotate secrets safely.
- edit the env file
- restart Gunicorn
- validate app health
- remove old credentials only after confirming the new ones work
- For containers, keep the same variable names.
If you deploy with Docker or Compose, pass variables through container environment settings or secrets and keep names aligned with Flask config.
Common Causes
- Variables exported in a shell only → systemd does not inherit them → define
EnvironmentFile=orEnvironment=in the service unit. - Wrong variable names in Flask config → app looks for
SECRET_KEYbut file definesSECRETorAPP_SECRET→ align names exactly. - Environment file not readable by the service → permission denied or wrong path → set correct ownership,
chmod 600, and verify the path in systemd. - Config loads too late in the app lifecycle → extensions initialize before env-backed settings are applied → load config before creating database, mail, cache, or login integrations.
- Invalid env file syntax → quotes, spaces, or
exportstatements break parsing → keep oneKEY=valuepair per line without shell-specific syntax. - Service not reloaded after changes → old values remain in memory → run
daemon-reloadwhen the unit changes and restart the service after env changes. - Using
.envin production without explicit loading → file exists but Flask never reads it → either load dotenv before app creation or use systemdEnvironmentFileinstead. - Secrets committed to Git → deployment works but creates a security risk → rotate exposed credentials and remove them from repository history if needed.
Debugging Section
Check service configuration, the running process environment, and startup logs.
Commands
sudo systemctl cat myflaskapp
sudo systemctl show myflaskapp --property=Environment
sudo systemctl status myflaskapp --no-pager
sudo journalctl -u myflaskapp -n 100 --no-pager
sudo grep -n 'EnvironmentFile' /etc/systemd/system/myflaskapp.service
sudo ls -l /etc/myflaskapp/myflaskapp.env
sudo cat /proc/$(pgrep -f 'gunicorn.*myflaskapp' | head -n1)/environ | tr '\0' '\n'
sudo -u www-data env | sort
python3 -c "import os; print(os.getenv('SECRET_KEY')); print(os.getenv('DATABASE_URL'))"
sudo systemd-analyze verify /etc/systemd/system/myflaskapp.service
What to look for
EnvironmentFile=is present in the active unit- systemd shows the expected environment variables
- Gunicorn starts without
KeyErroror config import failures - the env file path exists and permissions are correct
- variable names in Flask code exactly match variable names in the env file
- no invalid env file formatting such as
export KEY=value
If the service fails completely, check Flask Gunicorn Service Failed to Start. If variables still do not appear inside the service, use Flask Environment Variables Not Loading in Production.
Checklist
- Secrets are not hardcoded in the Flask repository.
- Production variables are stored in a dedicated file or secret store outside the app codebase.
-
EnvironmentFileis present in the systemd service. - File permissions on the secrets file are restricted.
- Gunicorn restarts successfully after config changes.
- The running process shows the expected environment variables.
- Flask connects successfully to the database and external services using those variables.
- Secret rotation procedure is documented and tested.
For broader deployment validation, review 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 Environment Variables Not Loading in Production
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: Should I store .env inside the project directory in production?
A: Prefer a root-owned file outside the repository, such as /etc/myflaskapp/myflaskapp.env.
Q: Does Nginx need these environment variables?
A: Usually no. Gunicorn and the Flask app need them. Nginx mainly proxies requests.
Q: Why do variables work in my shell but not in production?
A: systemd services do not inherit your interactive shell exports unless you define them in the service configuration.
Q: Can I use python-dotenv in production?
A: You can, but systemd EnvironmentFile or a dedicated secret manager is more predictable for server deployments.
Q: When do I need to restart the app?
A: After changing environment variables or secrets, restart the Gunicorn service so Flask reloads config at startup.
Final Takeaway
Production Flask apps should load secrets from the process environment, not from source code. On a VPS, the most reliable pattern is a locked-down env file referenced by systemd and consumed by Gunicorn at startup. If variables are missing, debug from the running service context, not from your shell.