Docker containers are not a silver bullet for isolating Python application dependencies.
Let’s see a Flask app running in production, not in some abstract docker run command.
Imagine you have a simple Flask app:
# app.py
from flask import Flask
import os
app = Flask(__name__)
@app.route('/')
def hello_world():
return f'Hello from container {os.environ.get("HOSTNAME", "unknown")}!'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
Now, to run this in production, we’ll use Gunicorn, a production-grade WSGI HTTP server, and Docker.
First, create a requirements.txt file:
Flask==2.3.2
gunicorn==20.1.0
Next, create a Dockerfile:
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Make port 8000 available to the world outside this container
EXPOSE 8000
# Run app.py using gunicorn when the container launches
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
Build the Docker image:
docker build -t my-flask-app .
And run the container:
docker run -d -p 5000:8000 --name flask-prod my-flask-app
Now, if you curl http://localhost:5000, you’ll see "Hello from container [container_id]!". This shows your Flask app is up and running, managed by Gunicorn inside a Docker container.
The core problem this solves is dependency hell. Without containers, managing different Python versions and their specific package requirements across development, staging, and production environments is a nightmare. Each environment needs its own virtualenv, and keeping them in sync is tedious. Docker provides a consistent, isolated environment for your application and its dependencies. Gunicorn handles the heavy lifting of serving your Flask app efficiently, dealing with worker processes, and managing requests, which flask run simply doesn’t do for production.
Internally, the Dockerfile defines the steps to build your application’s environment. FROM python:3.10-slim pulls a lightweight base image. WORKDIR /app sets the directory where your code will live. COPY . /app brings your app.py and requirements.txt into the container. RUN pip install installs the necessary Python packages. EXPOSE 8000 tells Docker that the container listens on port 8000. Finally, CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"] is the command that executes when the container starts, launching Gunicorn to serve your Flask application (app:app means the app object from the app.py file).
The docker run command then orchestrates this. -d runs the container in detached mode (in the background). -p 5000:8000 maps port 5000 on your host machine to port 8000 inside the container, allowing you to access your app via localhost:5000. --name flask-prod gives your running container a recognizable name.
A common point of confusion is the difference between flask run and Gunicorn. flask run is a development server. It’s designed for convenience during development, offering features like automatic reloads and debugging. It is not suitable for production because it’s single-threaded by default, doesn’t handle concurrent requests well, and lacks robust error handling and security features. Gunicorn, on the other hand, is a robust WSGI server built for production. It manages multiple worker processes to handle concurrent requests, can be configured with various worker types (sync, gevent, etc.), and is designed for stability and performance.
When you map ports with docker run -p, you are essentially creating a bridge. The host machine’s network interface listens on the specified host port (e.g., 5000), and any traffic arriving there is forwarded to the container’s network interface on the specified container port (e.g., 8000). This allows external access to services running inside isolated containers.
The CMD instruction in the Dockerfile is executed when the container starts. If you were to override it, for instance, by running docker run my-flask-app python app.py, you would bypass Gunicorn and run the Flask development server, which is precisely what you don’t want for production.
The next challenge is managing multiple instances of this container and routing traffic to them, which is where orchestration tools like Docker Compose or Kubernetes come into play.