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 the flask-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 and EXPOSE commands.

  • Use COPY instead of ADD unless you need the extra features of ADD 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:

Happy dockerizing!

Similar Posts