Optimising Flask Dockerfiles: Best Practices for DevOps and Developers
Step-by-step guidance on crafting Dockerfiles for Flask apps that reduce build times and deployment issues
💡Introduction
Welcome to the world of DevOps! 🚀 Today, we’re diving into an essential skill for any DevOps engineer: optimizing Dockerfiles for Flask applications. While beginner DevOps engineers often focus on mastering basic Dockerfile syntax, experienced engineers know that true expertise lies in optimization—crafting Dockerfiles that are efficient, secure, and production-ready.
In this blog, we’ll walk through the process of building a simple Flask application. First, we’ll create a basic Dockerfile, and then we’ll refine it into an optimized version, comparing the two to understand the difference. Whether you're a beginner or looking to sharpen your Dockerfile skills, this guide has something for everyone.
Let’s get started! 🛠️
💡Pre-Requisites
Before we dive into writing and optimising Dockerfiles for a Flask application, ensure you have the following prerequisites in place:
Basic Understanding of Flask
Familiarity with creating a simple Flask application will help you follow along seamlessly.Docker Installed
Make sure Docker is installed and running on your system. You can download it from the Docker website.Python Environment Setup
Python 3.x installed on your system, along withpip
for managing Python packages.Code Editor
Use any code editor of your choice, such as Visual Studio Code, PyCharm, or Sublime Text.Flask Installed
Install Flask in your Python environment using the command:pip install flask
Sample Flask Application
Have a simple Flask application ready or be prepared to create one as we proceed in the tutorial.
With these prerequisites in place, you’ll be ready to follow along and implement the Dockerfile optimisations we discuss. Let’s move on to the fun part!
💡 Creating the Flask Application
To start, we’ll create a simple Flask application and prepare it for containerisation. Follow these steps:
Create the Project Directory
Make a directory namedbasic-flask
and navigate into it.Create the Flask Application
Inside thebasic-flask
directory, create a file namedapp.py
with the following content:from flask import Flask app = Flask(__name__) @app.route("/") def HelloWorld(): return "Hello World" if __name__ == "__main__": app.run()
You can run this application using the command:
python3 app.py
Open your browser and go to
http://localhost:5000
. You should see Hello World displayed on the web page.List the Dependencies
To containerise the app, we first need to specify the required Python modules. Create arequirements.txt
file by running:pip3 freeze > requirements.txt
💡 Creating Dockerfiles
Now, let’s create two versions of Dockerfiles: a basic version and an optimised version.
Basic Dockerfile
The basic Dockerfile is straightforward but lacks efficiency and security optimisations:
FROM python:3.9-slim
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
CMD ["python3", "app.py"]
This Dockerfile is functional but leaves room for improvement in caching, size optimization, and security practices.
Optimised Dockerfile
The optimised Dockerfile follows multi-stage builds and incorporates best practices for efficiency, security, and modularity:
# syntax=docker/dockerfile:1.4
# Stage 1: Build dependencies
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
WORKDIR /code
# Install build dependencies and cache pip files for efficiency
COPY requirements.txt /code
RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install --prefix=/install -r requirements.txt
COPY . /code
# Stage 2: Development environment setup
FROM python:3.10-alpine AS dev-envs
WORKDIR /code
# Copy application files and installed dependencies
COPY --from=builder /install /usr/local
COPY . /code
# Install additional tools for development (e.g., Git, Bash)
RUN apk update && apk add --no-cache git bash
# Create a non-root user for better security
RUN addgroup -S docker && \
adduser -S --shell /bin/bash --ingroup docker vscode
# Set entrypoint and command for development purposes
ENTRYPOINT ["python3"]
CMD ["app.py"]
# Stage 3: Production-ready image
FROM python:3.10-alpine AS final
WORKDIR /app
# Copy only necessary application files and dependencies
COPY --from=builder /install /usr/local
COPY app.py /app
ENTRYPOINT ["python3"]
CMD ["app.py"]
Key Differences Between the Two Dockerfiles
Multi-Stage Build: The optimised Dockerfile uses multi-stage builds to reduce the final image size and ensure a clean separation of build and runtime environments.
Caching: It leverages
--mount=type=cache
to speed up pip installations by caching dependencies.Non-Root User: Adds a non-root user for better security.
Lightweight Image: Uses an Alpine base image to minimize the image size.
💡 Building the Dockerfiles
Now that we have created both Dockerfiles, it’s time to build Docker images and observe the differences in their sizes. Follow these steps:
Build the Image from the Basic Dockerfile
Ensure the content of the basic Dockerfile is saved in a file named
Dockerfile
.Build the image using the following command:
docker build -t basic-dockerfile .
Build the Image from the Optimised Dockerfile
Save the content of the optimised Dockerfile in a separate file named
Dockerfile
.Build the image using this command:
docker build -t optimised-dockerfile .
Compare the Built Images
Once the images are built, list all Docker images using:
docker images
You should notice a significant difference in the image sizes:
Basic Dockerfile Image: Approximately 177MB
Optimised Dockerfile Image: Approximately 59.2MB
Why the Optimised Image is Smaller
Lightweight Base Image: The optimised Dockerfile uses
python:3.10-alpine
, which is significantly smaller thanpython:3.9-slim
.Multi-Stage Build: Unnecessary build dependencies are excluded from the final image, keeping it minimal.
Efficient Caching: The use of caching for pip installations avoids redundant downloads and reduces image layers.
💡 Conclusion
Optimising Dockerfiles is a crucial skill for DevOps engineers aiming to create efficient, secure, and production-ready containers. In this blog, we explored how to build a simple Flask application, containerise it using a basic Dockerfile, and then refine it with an optimised Dockerfile.
The differences in image size and structure demonstrate the impact of best practices like using multi-stage builds, lightweight base images, and caching mechanisms. While the basic Dockerfile served its purpose, the optimised version provided a leaner, more secure, and performant container, highlighting the importance of thoughtful design in containerisation.
As you continue your DevOps journey, always strive to enhance your Dockerfiles by incorporating optimisations, considering security, and minimising overhead. A well-optimised Dockerfile not only saves time and resources but also ensures smoother deployments and scalability in production.
🚀 For more informative blog, Follow me on Hashnode, X(Twitter) and LinkedIn.
Happy coding and automating! 🚀