# Running a Flask App in a Docker Container: Common Pitfalls and Production Best Practices

> Learn to run Flask apps in Docker containers. Avoid common pitfalls like using the dev server and discover production best practices for secure and efficient deployments.

- Repository: [Pallets/flask](https://github.com/pallets/flask)
- Tags: best-practices
- Published: 2026-02-16

---

**Running a Flask app in a Docker container requires binding to `0.0.0.0`, using a production WSGI server instead of the built-in development server, and managing configuration through environment variables to avoid security vulnerabilities.**

Containerizing Python web applications is standard practice for modern deployments, but running a Flask app in a Docker container introduces specific networking, security, and configuration challenges that differ from local development. The pallets/flask repository provides the development tools and documentation that inform these best practices, yet many developers inadvertently ship containers with the debug server enabled or bind to localhost, rendering their services unreachable.

## Avoiding the Development Server in Production

The most critical mistake when running a Flask app in a Docker container is relying on `app.run()` or `flask run` in production. In [`src/flask/app.py`](https://github.com/pallets/flask/blob/main/src/flask/app.py), the `run` method is explicitly designed for development purposes only.

The built-in Werkzeug server is single-threaded, does not support graceful shutdown, and exposes the interactive debugger when `FLASK_ENV=development` or `DEBUG=True`. If an error occurs, the debugger allows arbitrary code execution, creating a severe security vulnerability in containerized environments.

**Best practice:** Treat `app.run` only for local testing. Deploy behind a production-grade WSGI server such as Gunicorn, uWSGI, or Waitress, and explicitly set `FLASK_ENV=production`.

## Network Binding and Port Exposure

A common networking pitfall when running a Flask app in a Docker container is binding to `127.0.0.1` (localhost). Inside a container, `127.0.0.1` is reachable only from the container itself; the host machine and external networks cannot reach the service.

According to the Flask quickstart documentation (`docs/quickstart.rst`), you must bind to `0.0.0.0` to listen on all interfaces, or use the `--host=0.0.0.0` flag when starting the application.

**Best practice:** Configure your WSGI server to bind to `0.0.0.0:8000` (or your chosen port), and use the `EXPOSE` directive in your Dockerfile for documentation purposes. Map the port when running the container using `-p 8000:8000` or the `ports` key in Docker Compose.

## Configuration Management and Secrets

Hard-coding configuration values such as `SECRET_KEY`, database URLs, or debug flags into your container image creates security risks and reduces portability. When these values are baked into the image, every environment (development, staging, production) requires a separate build, and secrets become visible in the image layers.

The Flask `run` method in [`src/flask/app.py`](https://github.com/pallets/flask/blob/main/src/flask/app.py) automatically loads environment variables from a `.env` file if python-dotenv is installed, but for containerized deployments, you should inject configuration at runtime.

**Best practice:** Load configuration from environment variables. Set `FLASK_ENV=production`, `SECRET_KEY`, and database connection strings via the `-e` flag in `docker run` or an `.env` file with Docker Compose. Never commit secrets to version control or bake them into images.

## Security Hardening and Image Optimization

Running containers as the `root` user violates the principle of least privilege. If your Flask process runs as root and is compromised, the attacker gains full container privileges and potentially host access.

Additionally, failing to use multi-stage builds results in bloated images containing development dependencies, build tools, and test files that increase the attack surface and deployment time.

**Best practice:** Create a non-root user in your Dockerfile using `RUN adduser --disabled-password --gecos "" appuser` and switch to it with `USER appuser`. Implement multi-stage builds: use a builder stage to compile dependencies and a runtime stage to copy only the necessary wheels and application code. Pin exact package versions in [`requirements.txt`](https://github.com/pallets/flask/blob/main/requirements.txt) to ensure reproducible builds.

## Static Files and Health Checks

Flask’s built-in static file handling (`/static` route) is designed for development convenience only. In production, serving static assets through a WSGI server like Gunicorn is inefficient and can block worker processes.

Additionally, container orchestrators like Kubernetes and Docker Swarm require health checks to determine if your application is ready to receive traffic. Without a dedicated endpoint, the orchestrator may restart healthy containers or fail to route traffic.

**Best practice:** Serve static files via a dedicated HTTP server like NGINX or Caddy, or configure your WSGI server’s static file handling. Implement a lightweight health-check endpoint in your Flask application:

```python
from flask import Flask

app = Flask(__name__)

@app.route("/health")
def health():
    return "", 200

```

Configure your orchestrator to poll this endpoint to verify container health.

## Graceful Shutdown and Process Management

When Docker stops a container, it sends a `SIGTERM` signal to the main process. If your Flask application runs under Gunicorn without proper timeout configuration, workers may be killed abruptly, causing dropped requests and incomplete transactions.

**Best practice:** Use Gunicorn’s `--graceful-timeout` flag to allow workers to finish processing in-flight requests before shutting down. Configure your container to handle `SIGTERM` properly by ensuring Gunicorn runs as PID 1 or using a lightweight init system like `tini`.

Example Gunicorn configuration:

```bash
gunicorn -b 0.0.0.0:8000 --workers 4 --graceful-timeout 30 "hello:create_app()"

```

## Summary

- **Never use the development server** (`app.run`) in production; deploy behind Gunicorn or uWSGI as implemented in [`src/flask/app.py`](https://github.com/pallets/flask/blob/main/src/flask/app.py).
- **Bind to `0.0.0.0`** instead of `127.0.0.1` to allow external access, as documented in `docs/quickstart.rst`.
- **Inject configuration via environment variables** rather than hard-coding secrets; set `FLASK_ENV=production` at runtime.
- **Run as a non-root user** and use multi-stage builds to minimize image size and attack surface.
- **Serve static files through a dedicated web server** and implement a `/health` endpoint for orchestrator compatibility.
- **Configure graceful shutdown** with `--graceful-timeout` to prevent dropped requests during container stops.

## Frequently Asked Questions

### Why does my Flask app work locally but return connection refused in Docker?

This occurs because Flask’s development server defaults to binding on `127.0.0.1` (localhost), which is only accessible inside the container itself. According to `docs/quickstart.rst`, you must specify `--host=0.0.0.0` or configure your WSGI server to bind to `0.0.0.0` to accept connections from outside the container.

### Is it safe to use `flask run` inside a Docker container?

No. The `flask run` command and `app.run()` method in [`src/flask/app.py`](https://github.com/pallets/flask/blob/main/src/flask/app.py) are designed specifically for development. They enable the interactive debugger and reloader, which expose arbitrary code execution vulnerabilities if enabled in production. Always use a production WSGI server like Gunicorn or uWSGI in containerized deployments.

### How do I handle secrets and configuration in a Flask Docker container?

Never bake secrets into your Docker image. Instead, pass configuration via environment variables at runtime using the `-e` flag or Docker Compose `environment` keys. Flask automatically loads variables from a `.env` file during development, but in containers, set `FLASK_ENV=production`, `SECRET_KEY`, and database URLs through the orchestration layer to keep credentials out of image layers.

### What is the correct way to shut down a Flask container without dropping requests?

Configure your WSGI server to handle `SIGTERM` signals gracefully. When Docker stops a container, it sends `SIGTERM` to PID 1. If using Gunicorn, set `--graceful-timeout 30` to allow workers to complete in-flight requests before the container exits. Ensure Gunicorn runs as the main process or use an init system like `tini` to properly forward signals to the worker processes.