Containerization has revolutionized the development world! Software developers use containerization to build and deploy applications across multiple environments without rewriting the program code. And Docker is the most popular tool for handling containerization. This is probably why, in most interviews, you will be asked about your experience working with Docker. Docker is a must-have!
Containers are lightweight software components that run efficiently. Wait, wait, wait—is that really so? Then what is this?
FROM python:3.13.1
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 1234
CMD ["python", "./main.py"]
Do you see the issue with this container? It will work and function properly, but it won’t be lightweight or efficient. So, how to improve this container?
Image Version
Let’s start with the first line of the container. The container size strongly depends on the image it is based on.
FROM python:3.13.1
This indicates that we are using the image with the 3.13.1 Python version, which is where the problem might be hidden. In general, if you are unsure about your specific needs, this is probably the one you want to use. But if that’s not the case for you, I would recommend exploring other options e.g.:
python:<version>-slim
- image excludes many common Debian packages to reduce size. It's a good choice for environments with space constraints or when only minimal packages are required.python:<version>-alpine
- image is based on the popular Alpine Linux project, available in the alpine official image. Alpine Linux is much smaller than most distribution base images (~5MB), and thus leads to much
slimmer images in general.
For my goals, I almost always use Alpine versions since they are purpose-built for containers and dramatically decrease the size of my container. So now my container will look like:
FROM python:3.13.1-alpine
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
EXPOSE 1234
CMD ["python", "./main.py"]
Layer Caching
Each instruction in the Dockerfile
creates a new layer. Right now, Docker does not reuse the layers from the previous build in the most efficient way because we copy all the code from the repository and only then installing the dependencies:
COPY . .
RUN pip install
To take advantage of caching, we will first copy the requirements.txt
file, then install the dependencies, and only after that, copy the source code from the repository ( in Node, do the same with package.json
):
FROM python:3.13.1-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 1234
CMD ["python", "./main.py"]
Dependencies change less frequently than source code, so now Docker can reuse the dependencies from the previous build, and only the source code needs to be rebuilt. There are three things that trigger cache invalidation in Docker:
- Changes to the file you are copying
- Changes to the
Dockerfile
instructions - Changes to any previous layer
Ignoring Unused
Right now, during the build process, Docker is copying everything from your folder into the container:
COPY . .
But we really don’t need everything, as some files and folders are unnecessary. Excluding them is pretty easy using the .dockerignore
file:
# Exclude Python bytecode files
*.pyc
*.pyo
__pycache__/
# Exclude Git files
.git/
# Exclude virtual environment directories
.venv/
env/
# Exclude local development files and logs
*.log
*.sqlite
*.db
# Exclude any IDE or editor specific files
.vscode/
.idea/
# Exclude node_modules
node_modules/
# Exclude test files and folders
tests/
test/
# Exclude the .env file (if sensitive data is inside)
.env
# Exclude Dockerfile and .dockerignore itself (optional, but often useful)
Dockerfile
.dockerignore
You may wonder why I included node_modules
here when the container is targeting Python. The answer is simple: it’s as heavy as your Ma... Sorry, I couldn’t hold myself back) But nevertheless, excluding node_modules
will greatly reduce the container size when you are using Node.
Layer Squashing
Let’s say I need to install the necessary build tools ( like gcc
and musl-dev
) to compile dependencies and remove them afterward. You could add just a few more instructions like this, but that wouldn’t be the best approach:
FROM python:3.13.1-alpine
WORKDIR /app
COPY requirements.txt .
RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev
RUN pip install --no-cache-dir -r requirements.txt
RUN apk del .build-deps
COPY . .
EXPOSE 1234
CMD ["python", "./main.py"]
Docker processes each RUN
command separately and does not save additional space in the final image. Each layer is immutable and contains only the changes from the previous layer. Separate RUN commands create additional layers, preserving files from previous layers. Deleting files from previous layers is impossible. That is basically means that we need to install and delete files in the same layer like:
FROM python:3.13.1-alpine
WORKDIR /app
COPY requirements.txt .
RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev \
&& pip install --no-cache-dir -r requirements.txt \
&& apk del .build-deps
COPY . .
EXPOSE 1234
CMD ["python", "./main.py"]
Multi-Stage Builds
When the application is built, we really only need to expose it to the world; we don't necessarily need Python or other dependencies. For this purpose, we can use NGINX to host our application:
FROM python:3.13.1-alpine AS builder
WORKDIR /app
COPY requirements.txt .
RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev \
&& pip install --no-cache-dir -r requirements.txt \
&& apk del .build-deps
COPY . .
RUN python ./main.py
FROM nginx:alpine
WORKDIR /usr/share/nginx/html
COPY --from=builder /app/output_dir .
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Here, the FROM
keyword separates the process into two different stages. This means that everything that happens in the first stage is discarded, and the final image only contains the build output and the NGINX image. As a result, the container is even smaller.
Conclusion
We reviewed the best practices for creating and optimizing Docker containers, and result you can see above. This container utilizes all the mentioned practices, and I really don’t know what else to add, so that’s it!
Hope your Christmas is very merry so as New Year! See you around folks!
Top comments (0)