How to Turn Off Flask Debug Mode in Production
Running Flask with debug=True in production turns on the Werkzeug interactive debugger and shows full tracebacks to anyone who triggers an error. A /debug route that returns os.environ is even worse - it hands out every secret. Delete the route, set debug to False, and confirm both leaks are closed. Here is how.
Two leaks that often travel together
During development it is normal to add helpers that make debugging easier. Two of them are dangerous when they reach production:
@app.route("/debug")
def debug_info():
return jsonify({
"environment": dict(os.environ),
"python_path": sys.path,
"working_directory": os.getcwd(),
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)
The /debug route returns every environment variable. In most deployments, that is
where the database password, API keys, and signing secrets live:
curl -s localhost:8000/debug | head -c 200
# {"environment":{"API_KEY":"sk-live-...","DB_PASSWORD":"...",...
debug=True does more than reload the code on save. It enables the Werkzeug
interactive debugger: when a request raises an exception, the browser shows the full
traceback with source code and local variables, and a console that can run Python inside
the server process. Werkzeug protects that console with a PIN, but the traceback page
itself already shows code and data, and a PIN is not a safe barrier on a public server.
Check whether the debugger is on
You do not need to cause an error to see it. The debugger serves its own static files, so a request for one of them tells you whether it is active:
curl -s "localhost:8000/health?__debugger__=yes&cmd=resource&f=debugger.js" | head -n 3
If you get JavaScript back, the debugger is live. If you get the normal response of the route, it is off.
Remove the debug route
Delete the whole route. Do not hide it behind a secret URL or a query parameter - a hidden route is found sooner or later by a scanner or a leaked log. If you really need runtime information in production, expose a small, safe set of values (version, uptime, health of dependencies), never the environment.
After you delete it, Flask returns 404 for the path:
curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/debug
# 404
Turn debug mode off
Change the app.run call:
app.run(host="0.0.0.0", port=8000, debug=False)
Debug mode is read when the server starts, so restart the process after the change. A code reload alone is not enough.
A better pattern is to never write debug=True in the code at all, and to control it
from the environment only in development:
import os
app.run(host="0.0.0.0", port=8000,
debug=os.environ.get("FLASK_DEBUG") == "1")
Or use the Flask CLI, where debug mode is a flag you pass on your own machine:
flask --app app run --debug # development only
Do not use the development server in production
app.run() starts the Werkzeug development server. It is meant for local work. In
production, run Flask behind a WSGI server such as gunicorn:
gunicorn --bind 0.0.0.0:8000 --workers 2 app:app
gunicorn imports the app object and does not call app.run(), so the interactive
debugger is never started. This is the strongest guard, because the production command
cannot turn the debugger on even if someone leaves debug=True in the file.
Verify everything still works
After the restart, check the three things that matter:
curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/debug # 404
curl -s "localhost:8000/health?__debugger__=yes&cmd=resource&f=debugger.js"
# {"status":"ok"} <- normal response, no debugger
curl -s localhost:8000/products # real data
If secrets were exposed
If the debug route was reachable from the internet, treat every value it returned as
leaked. Rotate the database password, API keys, and signing secrets, and check access
logs for requests to /debug. Removing the route stops future leaks, but it does not
undo the ones that already happened.
Keep it from coming back
- Search the code for
debug=True,/debug, andos.environreturned in a response before each release. A tool like Bandit flagsapp.run(debug=True)for you. - Return generic error pages in production (
{"error": "internal error"}) and send the full traceback to your logs, not to the client. - Keep secrets out of responses, logs, and error pages entirely.
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - freeWhat you'll practice
- Detecting a live Werkzeug debugger with a debugger resource request
- Removing an endpoint that dumps environment variables and secrets
- Turning off Flask debug mode and restarting the app so the change takes effect
FAQ
Is it safe to run Flask with debug=True in production?
No. Debug mode enables the Werkzeug interactive debugger, which shows tracebacks with source code and local variables and offers a Python console in the browser. Always run production with debug off, ideally under a WSGI server like gunicorn.
How do I check if the Flask debugger is enabled?
Request a debugger resource on any route, for example /health?__debugger__=yes&cmd=resource&f=debugger.js. If JavaScript comes back, the debugger is active. If you get the route's normal response, it is off.
Does the Werkzeug debugger PIN make debug mode safe?
No. The PIN only guards the console. Traceback pages still show code and data, and a PIN is a weak control on a public server. The right setting is debug off in production.
What should I do if a debug endpoint exposed my environment variables?
Remove the endpoint, then rotate every secret it returned - database passwords, API keys, and signing keys - and review access logs for requests to it.
Keep learning
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - free