How to Dockerize a Flask Application: A Comprehensive Guide
Flask is a popular lightweight web framework for Python known for its simplicity and flexibility. Dockerizing a Flask application allows you to package it along with its dependencies into a standardized, portable unit that can be consistently deployed across environments.
In this in-depth guide, we‘ll cover everything you need to know to dockerize a Flask app effectively. We‘ll start by walking through a hands-on example of containerizing a simple Flask app. Then we‘ll dive into key Docker concepts, best practices, and how containerization fits into the modern application development landscape.
Whether you‘re a seasoned full-stack developer looking to containerize an existing Flask app, or a beginner looking to learn Docker, this guide has you covered. Let‘s jump in!
Why Dockerize Flask Apps?
Before we get into the how, let‘s talk about the why. Dockerizing your Flask application provides several compelling benefits:
-
Consistency across environments: Packaging your app and its dependencies together in a container ensures that it runs consistently across development, staging, and production environments.
-
Portability: Docker containers can be run on any platform that supports Docker, making your application highly portable.
-
Isolation: Containers provide a level of isolation between applications, reducing conflicts between dependencies and allowing multiple apps to run on the same host.
-
Efficiency: Containers are lightweight and share the host machine‘s OS kernel, resulting in less overhead compared to virtual machines.
-
Scalability: Dockerized applications can be easily scaled horizontally by spinning up additional container instances as needed.
-
Faster deployment: Packaging your app as a Docker image allows you to automate and streamline the deployment process.
The adoption of Docker has exploded in recent years. Over 13 billion Docker image pulls occurred in 2021 alone, with over 13 million developers now using Docker regularly (source). For Python specifically, Docker is now the most popular way to package Python applications for deployment (source).
Key Docker Concepts
Before we dive into dockerizing a Flask app, let‘s review some key Docker concepts.
-
Dockerfile: A text file that contains instructions for building a Docker image. It specifies the base image, application dependencies, and startup command.
-
Image: A read-only template that packages an application along with its dependencies and configuration. Images are built from Dockerfiles and stored in registries.
-
Container: A runnable instance of an image. Containers are isolated from each other and the host machine, but share the host‘s OS kernel.
-
Registry: A repository for storing and distributing Docker images. Docker Hub is the default public registry.
-
Layer: A set of read-only files or commands that describe a portion of an image. Layers are stacked on top of each other to form a complete image.
-
Volume: A way to persist data generated by and used by containers. Volumes exist independently of containers and can be mounted into multiple containers simultaneously.
Docker uses a client-server architecture. The Docker client talks to the Docker daemon (server), which is responsible for building, running, and distributing containers.
Steps to Dockerize a Flask Application
Now that we‘ve covered the key concepts, let‘s walk through the steps to dockerize a basic Flask application.
1. Set up the Flask application
First, create a new directory for your project and navigate into it:
mkdir flask-docker
cd flask-docker
Create a new file named app.py
with the following contents:
from flask import Flask
app = Flask(__name__)
@app.route(‘/‘)
def hello():
return ‘‘
if __name__ == ‘__main__‘:
app.run(debug=True, host=‘0.0.0.0‘)
This defines a basic Flask app with a single route that returns a simple HTML heading.
Next, create a requirements.txt
file to specify the app‘s Python dependencies:
Flask==2.1.0
2. Write the Dockerfile
In the project directory, create a new file named Dockerfile
(without any extension). Add the following contents:
# syntax=docker/dockerfile:1
FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]
Let‘s break this down:
FROM python:3.9-slim-buster
specifies an official Python base image to build on top of. Using a slim variant reduces the final image size.WORKDIR /app
sets the working directory in the image to/app
.COPY requirements.txt requirements.txt
copies the requirements file into the image.RUN pip3 install -r requirements.txt
installs the Python dependencies.COPY . .
copies the rest of the application code into the image.CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0"]
specifies the command to run when the container starts. This runs the Flask app and makes it externally accessible.
3. Build the Docker Image
With the Dockerfile written, you can now build the Docker image. Run the following command from the project directory:
docker build -t flask-demo .
This command builds an image tagged as flask-demo
based on the current directory‘s Dockerfile. The .
at the end specifies the build context (current directory).
Building the image may take a minute or two as Docker downloads the base image and installs the dependencies. You should see output similar to:
Sending build context to Docker daemon 8.192kB
Step 1/6 : FROM python:3.9-slim-buster
...
Successfully built d1ea5e3c3f51
Successfully tagged flask-demo:latest
You can verify that the image was created by running docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-demo latest d1ea5e3c3f51 16 seconds ago 123MB
4. Run the Docker Container
With the image built, you can now spin up a container from it. Run the following command:
docker run -d -p 5000:5000 --name flask-app flask-demo
Let‘s dissect this command:
-d
runs the container in detached mode (background).-p 5000:5000
maps port 5000 from the container to port 5000 on the host. This makes the Flask app accessible from outside the container.--name flask-app
assigns a friendly name to the container for easier reference.flask-demo
specifies the image to create the container from.
You can verify the container is running with docker ps
:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f9b1f7d51c35 flask-demo "python3 -m flask ru…" 5 seconds ago Up 4 seconds 0.0.0.0:5000->5000/tcp flask-app
Now, open a web browser and navigate to http://localhost:5000
. You should see the "Hello from Flask & Docker!" message, confirming that the dockerized Flask app is running successfully.
5. Additional Docker Commands
Here are some other handy Docker commands to know:
docker stop flask-app
: Stops the running container.docker start flask-app
: Starts a stopped container.docker rm flask-app
: Removes a stopped container.docker rmi flask-demo
: Deletes theflask-demo
image.docker exec -it flask-app /bin/bash
: Opens an interactive shell inside the running container for debugging.docker logs flask-app
: Shows the logs from the container.docker system prune
: Removes all stopped containers, unused networks, and dangling images to free up space.
Dockerfile Best Practices & Optimizations
Writing efficient Dockerfiles is crucial for creating lightweight, secure, and performant images. Here are some best practices to follow:
-
Use official, minimal base images to reduce attack surface and image size. For Python, prefer slim or alpine variants.
-
Order Dockerfile commands from least to most frequently changing to leverage layer caching and speed up builds. For example, copy in
requirements.txt
and install dependencies before copying in the rest of the application code. -
Combine related commands into a single
RUN
statement to reduce the number of layers. For example:RUN apt-get update && \ apt-get install -y git && \ rm -rf /var/lib/apt/lists/*
-
Use multi-stage builds to create separate build and runtime environments. This allows you to use larger base images with build tools during the build stage, but copy only the compiled artifacts into a minimal runtime image.
-
Avoid including unnecessary files by using a
.dockerignore
file. This is similar to.gitignore
and specifies files and directories that should be excluded when copying files into the image. -
Set default environment variables and expose only necessary ports with
ENV
andEXPOSE
commands. -
Use
COPY
instead ofADD
unless you need the extra features ofADD
like remote URL support and local tar extraction. -
Avoid running the app as
root
inside the container. Create and switch to a non-root user for improved security.
Here‘s an example of an optimized multi-stage Dockerfile for a Flask app:
# Build stage
FROM python:3.9-slim-buster AS build
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Runtime stage
FROM python:3.9-slim-buster
WORKDIR /app
COPY --from=build /app/wheels /wheels
COPY --from=build /app/requirements.txt .
RUN pip install --no-cache /wheels/*
COPY . .
ENV FLASK_ENV production
EXPOSE 5000
CMD ["python", "app.py"]
This Dockerfile uses a separate build stage to compile the Python dependencies into wheels, then copies only the wheels into the final runtime image. This results in a smaller final image size.
Dockerizing Flask with a Database
Most real-world Flask applications interact with a database. Let‘s walk through an example of dockerizing a Flask app that uses a PostgreSQL database.
First, update the Flask app to connect to the database:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config[‘SQLALCHEMY_DATABASE_URI‘] = ‘postgresql://user:pass@db/mydb‘
db = SQLAlchemy(app)
@app.route(‘/‘)
def hello():
return ‘Hello from Flask & Docker!‘
if __name__ == ‘__main__‘:
app.run(host=‘0.0.0.0‘)
This code uses the Flask-SQLAlchemy
extension to connect to a PostgreSQL database running in a separate container.
Next, create a docker-compose.yml
file to define the multi-container setup:
version: ‘3‘
services:
web:
build: .
ports:
- "5000:5000"
depends_on:
- db
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://user:pass@db/mydb
db:
image: postgres:13
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
This file defines two services: web
for the Flask app and db
for the PostgreSQL database. The web
service depends on the db
service and is connected to it via the DATABASE_URL
environment variable.
To start the multi-container application, run:
docker-compose up -d
This command builds the Flask app image and starts both containers. The -d
flag runs the containers in detached mode.
You can now access the Flask app at http://localhost:5000
, and it will be able to connect to the PostgreSQL database running in the db
container.
To stop the containers, run:
docker-compose down
Using docker-compose
is a convenient way to manage multi-container applications and define their configuration in a single file.
Deployment and CI/CD
With your Flask application dockerized, you can now deploy it to various environments with ease. Docker images can be pushed to a registry like Docker Hub or a private registry, and then pulled and run on any host with Docker installed.
Integrating Docker into your continuous integration and deployment (CI/CD) pipeline allows you to automate the process of building, testing, and deploying your Flask application.
Here‘s an example of a simple CI/CD pipeline using GitLab CI/CD:
stages:
- build
- test
- deploy
build:
stage: build
script:
- docker build -t my-flask-app .
- docker push my-flask-app
test:
stage: test
script:
- docker run my-flask-app python -m unittest discover
deploy:
stage: deploy
script:
- docker pull my-flask-app
- docker stop my-flask-app || true
- docker rm my-flask-app || true
- docker run -d --name my-flask-app -p 5000:5000 my-flask-app
This .gitlab-ci.yml
file defines three stages: build, test, and deploy. The build stage builds the Docker image and pushes it to a registry. The test stage runs the Flask app‘s unit tests inside a container. Finally, the deploy stage pulls the latest image, stops and removes any existing container, and starts a new container from the latest image.
Other popular CI/CD tools like Jenkins, CircleCI, and Travis CI also have excellent support for building and deploying Docker images.
Conclusion
In this comprehensive guide, we‘ve covered everything you need to know to dockerize a Flask application effectively. We started by walking through a hands-on example of containerizing a basic Flask app. Then we explored key Docker concepts, best practices for writing Dockerfiles, and how to optimize images for size and performance.
We also discussed how to dockerize Flask apps that depend on databases using docker-compose
, and how to integrate containerized Flask apps into a CI/CD pipeline for automated testing and deployment.
Containerization has become a crucial skill for modern application development, and Docker is the most widely used container platform. By leveraging Docker to package your Flask applications, you can ensure consistency across environments, simplify deployments, and take advantage of the portability and scalability benefits of containers.
To learn more about Docker and containerizing Python applications, check out the following resources:
- Official Docker Documentation
- Dockerizing a Python Web Application
- Flask Tutorial – Dockerize Your Application
- Best practices for writing Dockerfiles
Happy dockerizing!