Skip to main content

Flask: Containerizing with Docker

Flask: Containerizing with Docker

Containerizing a Flask application with Docker simplifies deployment, ensures consistency across environments, and enhances scalability. Flask, built on a lightweight core with Jinja2 Templating and Werkzeug WSGI, integrates seamlessly with Docker to package applications, dependencies, and configurations into portable containers. This tutorial covers Flask containerizing with Docker, providing a step-by-step guide to creating, building, and running a Flask app in a Docker container, along with best practices for production-ready deployments.


01. Why Containerize with Docker?

Docker encapsulates a Flask application and its dependencies (e.g., Python, libraries, Gunicorn) into a container, ensuring identical behavior in development, testing, and production. Combined with Flask’s compatibility with Werkzeug WSGI for request handling and Jinja2 Templating for rendering, Docker simplifies deployment to platforms like Kubernetes, AWS ECS, or local servers, while improving portability and isolation.

Example: Basic Flask App

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello, Docker!'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Explanation:

  • A minimal Flask app for containerization.
  • host='0.0.0.0' ensures the app is accessible outside the container.

02. Prerequisites

Ensure the following are installed:

  • Docker: Install Docker Desktop (Windows/Mac) or Docker Engine (Linux) from docker.com.
  • Python 3.10+: For local development and testing.
  • Git (Optional): For version control.

03. Step-by-Step Guide to Containerizing Flask

This guide walks through creating a Dockerized Flask application, including a production-ready setup with Gunicorn.

3.1 Set Up the Flask Application

Example: Project Setup

# Create project directory
mkdir flask-docker
cd flask-docker

# Create virtual environment and install dependencies
python3 -m venv venv
source venv/bin/activate  # Linux/Mac
venv\Scripts\activate  # Windows
pip install flask gunicorn
pip freeze > requirements.txt

requirements.txt (Generated):

Flask==3.0.3
gunicorn==22.0.0
Jinja2==3.1.4
Werkzeug==3.0.4
...

Explanation:

  • Creates a project with Flask and Gunicorn (production WSGI server).
  • requirements.txt lists dependencies for Docker.

3.2 Create a Dockerfile

Example: Dockerfile

# Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV FLASK_ENV=production
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Explanation:

  • FROM python:3.12-slim: Uses a lightweight Python 3.12 image.
  • WORKDIR /app: Sets the working directory inside the container.
  • COPY requirements.txt: Copies dependencies first for caching.
  • RUN pip install: Installs dependencies.
  • CMD: Runs Gunicorn to serve the Flask app on port 8000.

3.3 Update Flask App for Production

Example: Production-Ready Flask App

# app.py
from flask import Flask
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-secret')

@app.route('/')
def index():
    return 'Hello, Docker!'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))

Explanation:

  • Uses environment variables for SECRET_KEY and PORT.
  • Allows flexibility for local testing and production.

3.4 Build and Run the Docker Container

Example: Build and Run

# Build Docker image
docker build -t flask-docker .

# Run container
docker run -p 8000:8000 -e SECRET_KEY=my-secure-key flask-docker

Access the App:

Open http://localhost:8000 in a browser

Output:

Hello, Docker!

Explanation:

  • docker build: Creates an image named flask-docker.
  • docker run: Maps host port 8000 to container port 8000 and sets environment variables.

3.5 Use Docker Compose for Development

Example: docker-compose.yml

# docker-compose.yml
version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - SECRET_KEY=my-secure-key
      - FLASK_ENV=development
    volumes:
      - .:/app

Run with Docker Compose:

docker-compose up --build

Explanation:

  • docker-compose.yml: Defines services, ports, and environment variables.
  • volumes: Mounts the local directory for live code changes in development.

3.6 Add a Database (e.g., PostgreSQL)

Example: Flask with PostgreSQL

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-secret')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)

@app.route('/')
def index():
    users = User.query.all()
    return f'Users: {[user.username for user in users]}'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))

Updated requirements.txt:

pip install flask-sqlalchemy psycopg2-binary
pip freeze > requirements.txt

Updated docker-compose.yml:

version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - SECRET_KEY=my-secure-key
      - DATABASE_URL=postgresql://user:password@db:5432/flaskdb
    depends_on:
      - db
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=flaskdb
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Run and Initialize Database:

docker-compose up --build
# In another terminal
docker-compose exec web flask db init  # If using Flask-Migrate

Explanation:

  • Adds a PostgreSQL service linked to the Flask app.
  • depends_on: Ensures the database starts first.
  • Persists database data with a Docker volume.

3.7 Common Issues

Example: Port Binding Error

Error: Bind for 0.0.0.0:8000 failed: port is already allocated

Solution:

docker ps  # Check running containers
docker stop <container-id>
docker run -p 8000:8000 flask-docker

Explanation:

  • Ensure no other process uses port 8000.
  • Use docker logs <container-id> for debugging.

04. Best Practices

  • Minimize Image Size: Use python:3.12-slim and --no-cache-dir for pip.
  • Environment Variables: Store sensitive data (e.g., SECRET_KEY) in .env or Docker Compose.
  • .gitignore: Exclude venv/, .env, and *.pyc.
  • # .gitignore
    venv/
    .env
    *.pyc
    
  • Multi-Stage Builds: For larger apps, use multi-stage builds to reduce image size.
  • Health Checks: Add HEALTHCHECK in Dockerfile for production.

4.1 Multi-Stage Build Example

Example: Multi-Stage Dockerfile

# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Production stage
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . .
ENV FLASK_ENV=production
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Explanation:

  • Separates build and runtime environments to reduce image size.
  • Copies only necessary dependencies.

4.2 Comprehensive Example

Example: Full Flask App with PostgreSQL

# app.py
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-secret')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)

@app.route('/')
def index():
    users = User.query.all()
    return render_template('index.html', users=users)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))

templates/index.html:

<!DOCTYPE html>
<html>
<head><title>Flask Docker</title></head>
<body>
  <h1>Users</h1>
  <ul style="padding: 0px 0px 0px 20px; margin-top: 0px;">{% for user in users %}<li>{{ user.username }}</li>{% endfor %}</ul>
</body>
</html>

Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_ENV=production
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

docker-compose.yml:

version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - SECRET_KEY=my-secure-key
      - DATABASE_URL=postgresql://user:password@db:5432/flaskdb
    depends_on:
      - db
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=flaskdb
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Run:

docker-compose up --build

Explanation:

  • Combines Flask, PostgreSQL, and Gunicorn in a Dockerized setup.
  • Uses Jinja2 Templating for rendering user data.

05. Common Use Cases

5.1 Deploying a Web Application

Example: Web App with Static Files

# app.py
from flask import Flask, render_template
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-secret')

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))

templates/index.html:

<!DOCTYPE html>
<html>
<head><title>Flask Docker</title><link rel="stylesheet" href="/static/style.css"></head>
<body><h1>Dockerized Flask App</h1></body>
</html>

Explanation:

  • Serves static files (e.g., style.css) and templates.
  • Containerized for consistent deployment.

5.2 Deploying an API

Example: Flask API

# app.py
from flask import Flask, jsonify
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-secret')

@app.route('/api/data')
def data():
    return jsonify({'message': 'Dockerized Flask API'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))

Explanation:

  • Deploys a RESTful API in a Docker container.
  • Scalable with orchestration tools like Kubernetes.

Conclusion

Containerizing Flask applications with Docker, powered by Jinja2 Templating and Werkzeug WSGI, ensures portability, consistency, and scalability. Key takeaways:

  • Use a lightweight base image and Gunicorn for production.
  • Leverage Docker Compose for multi-container setups (e.g., with PostgreSQL).
  • Secure configurations with environment variables.
  • Optimize with multi-stage builds and health checks.

Dockerized Flask applications are ready for deployment to any platform, streamlining development and production workflows with minimal overhead!

Comments