Migrating to Python Chainguard Images
This guide is a high-level overview for migrating an existing containerized Python application to Chainguard Images.
Chainguard Images are built on Wolfi, a distroless Linux distribution designed for security and a reduced attack surface. Chainguard Images are smaller and have low to no CVE. Our Chainguard Images for Python are built nightly for extra freshness, so they’re always up-to-date with the latest remediations.
What is Distroless?
Distroless images are minimalist container images containing only essential software required to build or execute an application. That means no package manager, no shell, and no bloat from software that only makes sense on bare metal servers.What is Wolfi OS?
Wolfi is a community Linux undistro created specifically for containers. This brings distroless to a new level, including additional features targeted at securing the software supply chain of your application environment: comprehensive SBOMs, signatures, daily updates, and timely CVE fixes.Because Chainguard Images aim to be minimal, including providing separate development and production tags, adapting your containerized application requires that you consider some additional factors that will be discussed below.
Chainguard Images for Python Overview
We distribute two versions of our Python Chainguard Image: a development image that includes shells such as ash/bash and package managers such as pip and a production image that removes these tools for increased security. Ourpublic production images are tagged as latest
, while our public development images are tagged as latest-dev
.
Differences from the Docker Official Image
When migrating your Python application , keep in mind these differences between the Chainguard Image for Python and the official Docker image.
- The entrypoint for the Chainguard Image for Python is
/usr/bin/python
. When running either thelatest
orlatest-dev
versions of the image interactively, you’ll be working in the Python interpreter. When usingCMD
in your Dockerfiles, provided commands will be passed topython
by default. If you change the path to include binaries from a virtual environment , you should manually set the entrypoint or your Dockerfile will continue to use the included system Python as the entrypoint and you will not have access to installed packages in the virtual environment. - Chainguard Images for Python run as the
nonroot
user by default. If you need elevated permissions, such as to add packages withapk
, run the image as--user root
. You should not use the root user in a production scenario. - The
/home
and/home/nonroot
directories are owned by the nonroot user. - The
python:latest
Chainguard Image intended for production does not include ash
,ash
, orbash
. See the Debugging Distroless guide for advice on resolving issues without the use of these shells. - The
python:latest
Chainguard Image does not contain package managers such aspip
orapk
. See the sections below for guidance on multi-stage builds (recommended)or building your own images on Wolfi (advanced usage). - Chainguard Images for Python aim to be lightweight, and you may find that specific packages or dependencies are not included by default. The image details reference provides specific information on packages, features, and default environment variables for the image.
Migrating a Python Application
When migrating most containerized Python applications, we recommend building a virtual environment with any needed Python packages using our provided development images, then copying over the virtual environment to our stripped-down production image. Chainguard Academy hosts detailed instructions for a multi-stage build for a CLI-based Python script.
The below Dockerfile provides an example of such a multi-stage build for a simple Flask application. You can view a version of this Dockerfile with included sample Flask application and requirements.txt
in this repository, and the original unmigrated application in the v0 branch. A more complex setup with reverse proxy orchestrated with Docker Compose is provided in the next section.
# syntax=docker/dockerfile:1
FROM cgr.dev/chainguard/python:latest-dev as dev
WORKDIR /flask-app
RUN python -m venv venv
ENV PATH="/flask-app/venv/bin":$PATH
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
FROM cgr.dev/chainguard/python:latest
WORKDIR /flask-app
COPY app.py app.py
COPY --from=dev /flask-app/venv /flask-app/venv
ENV PATH="/flask-app/venv/bin:$PATH"
EXPOSE 8000
ENTRYPOINT ["python", "-m", "gunicorn", "-b", "0.0.0.0:8000", "app:app"]
When running an application containerized with the above Dockerfile, the application should be visible on 0.0.0.0:8000
.
As you can see, the primary difference in this Flask application compared to the pre-migration application is the use of a multistage build. In the initial stage, we copy our requirements into the development version of the Python Chainguard Image, initialize a virtual environment, and install needed packages with pip. In the second stage, we copy the virtual environment from the development image, copy the application from the host, set exposed port metadata, and run the application with the Gunicorn WSGI server.
By default, the entrypoint for the Python Chainguard Image is /usr/bin/python
rather than bash
. However, if you shadow the included system python
with the virtual environment python
on the path as we do above, you should set the entrypoint explicitly. Otherwise, you will not have access to the packages included in your virtual environment.
We recommend that you pin dependencies to specific versions in your own application. The example Flask application script linked above also enables debug mode, which should be turned off in a production scenario.
You may wish to include the following environmental variables in your Dockerfile. The first prevents the buffering of output, meaning that all messages printed to standard output are immediately printed rather than being held in a cache. The second prevents the creation of cached bytecode, which can marginally reduce image size.
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
Serving an Application with nginx and Docker Compose
We provide an nginx Chainguard Image, also with low to no CVEs, that can be used as a secure and performant reverse proxy to serve your application. You can view an example orchestration of a Flask application and nginx using Chainguard Images at the linked repository. The compose.yml
file is provided as a reference below.
services:
flask-app:
build:
context: flask-app
restart: always
ports:
- 8000:8000
networks:
- backnet
- frontnet
nginx:
build: nginx
restart: always
ports:
- 80:80
depends_on:
- flask-app
networks:
- frontnet
networks:
backnet:
frontnet:
The backnet and frontnet networks are provided in anticipation of other backend services such as a database container. View the sample repository branch for a full orchestration example with nginx configuration.
Advanced Usage
If your project image requires a set of packages that cannot be installed with pip using the multi-stage approach above, you can consider building your application on the Wolfi base image and installing additional Python and non-Python packages as APKs.
Additional Resources
You may wish to refer to the Python microservice example in the porting a sample application guide as an additional useful reference while migrating your application.
Debugging distroless containers can be a challenge given their lack of interactive tools such as shells. If you’re having difficulty debugging issues with your multi-stage build, you may find the Debugging Distroless guide a useful resource.
The following blog posts and videos may also assist with migrating your Python application:
- Blog Post: Securely Containerize a Python Application with Chainguard Images
- Video: How to containerize a Python application with a multi-stage build using Chainguard Images
Last updated: 2024-05-02 15:06