Migrating to Python Chainguard Images

Guide on migrating containerized Python applications to 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 the latest or latest-dev versions of the image interactively, you’ll be working in the Python interpreter. When using CMD in your Dockerfiles, provided commands will be passed to python 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 with apk, 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 a sh, ash, or bash. 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 as pip or apk. 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 pythonon 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:

Last updated: 2024-05-02 15:06