Jason Miller / Nuitka + Docker: Containerizing Compiled Python Applications

Created Tue, 18 Aug 2026 01:00:00 -0700 Modified Tue, 18 Aug 2026 22:01:35 +0000

Nuitka + Docker: Building Minimal, Compiled Python Containers

A standard Python Docker image for a FastAPI application typically ranges from 150 MB to 1 GB, depending on the base image and dependencies. A Nuitka-compiled equivalent can drop to under 40 MB using a distroless base, with faster startup and no Python interpreter exposed in the container.

This guide covers the multi-stage build pattern that makes this work reliably.


Why Compile Before Containerizing?

The standard Python container workflow copies source files into an image with a full Python runtime. This creates several problems at scale:

  • Large image size: CPython + pip + dependencies + source = bloat
  • Slow cold starts: Kubernetes pod scheduling with large images increases time-to-ready
  • Exposed source: anyone with image pull access can read your application logic
  • Runtime attack surface: a full Python interpreter in the container is a useful tool for an attacker post-exploitation

Nuitka-compiled containers eliminate all of these. The compiled binary is the entire application; no interpreter, no source, no pip.


Prerequisites

You need the following on your build machine (or CI runner):

# Install Nuitka
pip install nuitka

# Required C compiler (Linux)
apt-get install -y gcc patchelf ccache

# Required C compiler (macOS)
xcode-select --install

The final Docker image does not need Python or gcc — compilation happens in the build stage.


Multi-Stage Dockerfile: FastAPI Application

# ── Stage 1: Compile ────────────────────────────────────────────────────────
FROM python:3.12-slim AS builder

WORKDIR /build

# Install C compiler and Nuitka dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    patchelf \
    ccache \
    libffi-dev \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application source
COPY . .

# Compile with Nuitka
RUN python -m nuitka \
    --standalone \
    --onefile \
    --include-package=fastapi \
    --include-package=uvicorn \
    --include-package=pydantic \
    --include-package=starlette \
    --output-filename=app \
    main.py

# ── Stage 2: Runtime ─────────────────────────────────────────────────────────
FROM debian:bookworm-slim AS runtime

WORKDIR /app

# Copy only the compiled binary
COPY --from=builder /build/app .

# Non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

EXPOSE 8000

CMD ["/app/app"]

The runtime stage contains nothing but the compiled binary and a minimal Debian base. No Python, no pip, no source files.

The Application Entry Point

Your main.py must launch Uvicorn programmatically, not via the CLI. Nuitka cannot resolve CLI-style uvicorn main:app module references:

# main.py
import uvicorn
from api import app  # your FastAPI application

if __name__ == "__main__":
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        workers=1,  # single worker — Nuitka + Uvicorn multiprocessing is unstable
        access_log=True,
    )

Multi-Stage Dockerfile: Flask + Waitress

Flask with the Waitress WSGI server is simpler because Waitress uses threads instead of processes, which compiles more cleanly:

# ── Stage 1: Compile ────────────────────────────────────────────────────────
FROM python:3.12-slim AS builder

WORKDIR /build

RUN apt-get update && apt-get install -y gcc patchelf ccache \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Include template and static directories
RUN python -m nuitka \
    --standalone \
    --onefile \
    --include-data-dir=templates=templates \
    --include-data-dir=static=static \
    --include-package=waitress \
    --include-package=flask \
    --output-filename=app \
    main.py

# ── Stage 2: Runtime ─────────────────────────────────────────────────────────
FROM debian:bookworm-slim AS runtime

WORKDIR /app

RUN groupadd -r appuser && useradd -r -g appuser appuser

COPY --from=builder /build/app .
USER appuser

EXPOSE 8080

CMD ["/app/app"]

Flask entry point:

# main.py
from waitress import serve
from myapp import create_app

if __name__ == "__main__":
    app = create_app()
    serve(app, host="0.0.0.0", port=8080, threads=8)

Using Distroless for Minimal Attack Surface

Replace the debian:bookworm-slim runtime base with Google’s distroless image to remove all shells and package managers:

FROM gcr.io/distroless/cc-debian12 AS runtime

WORKDIR /app
COPY --from=builder /build/app .

EXPOSE 8000
ENTRYPOINT ["/app/app"]

distroless/cc includes the C standard library (libc) required by Nuitka-compiled binaries. It has no shell, no package manager, and no Python — which means docker exec into the container for debugging is not available. Plan your observability strategy accordingly (structured logging to stdout, distributed tracing).


Image Size Comparison

Approach Base Image Approximate Size
CPython + source python:3.12 900 MB – 1.2 GB
CPython + source python:3.12-slim 150–300 MB
Nuitka compiled debian:bookworm-slim 60–100 MB
Nuitka compiled distroless/cc 35–60 MB

The range depends on the number of dependencies your application compiles in. Pure-Python dependencies become compiled C code; native C extensions are included as-is.


GitHub Actions CI/CD Pipeline

Complete pipeline that builds the Docker image using Nuitka compilation:

# .github/workflows/build.yml
name: Build and Push

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        if: github.ref == 'refs/heads/main'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=ref,event=branch
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.ref == 'refs/heads/main' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          # BuildKit cache speeds up repeated Nuitka C compilation
          build-args: |
            BUILDKIT_INLINE_CACHE=1

Caching Nuitka’s C Compilation

Nuitka’s primary cost is the C compilation step. Enable ccache in your Dockerfile builder stage to cache this across builds:

FROM python:3.12-slim AS builder

# Set ccache directory
ENV CCACHE_DIR=/ccache
ENV PATH="/usr/lib/ccache:${PATH}"

RUN apt-get update && apt-get install -y gcc patchelf ccache

# In GitHub Actions, cache the ccache directory
# Add to your workflow:
# - uses: actions/cache@v4
#   with:
#     path: /tmp/.buildx-cache
#     key: nuitka-ccache-${{ runner.os }}-${{ hashFiles('requirements.txt') }}

With ccache enabled and BuildKit layer caching, subsequent builds of an unchanged codebase take 30–60 seconds instead of 5–10 minutes.


Kubernetes Deployment

The compiled container works identically to any other container in Kubernetes. The main considerations:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: ghcr.io/yourorg/api:latest
          ports:
            - containerPort: 8000
          resources:
            requests:
              memory: "64Mi"    # lower than equivalent CPython container
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "500m"
          # Compiled binary starts fast; readiness probe can be aggressive
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 1   # vs 5-10s for Python containers
            periodSeconds: 5
          # Security context: no shell available anyway, enforce it
          securityContext:
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            runAsUser: 1000
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]

The initialDelaySeconds: 1 on the readiness probe reflects Nuitka’s fast startup. CPython containers typically need 5–10 seconds before the application is ready to serve traffic.


Debugging Without a Shell

Since distroless containers have no shell, debugging requires alternative approaches:

# Copy files out of a running container for inspection
kubectl cp api-pod-xxx:/app/app ./app-binary

# Stream logs (your only real-time insight)
kubectl logs -f deployment/api

# Use ephemeral debug containers (Kubernetes 1.23+)
kubectl debug -it api-pod-xxx --image=busybox --target=api

Structure your application to log to stdout in JSON format so log aggregators (Datadog, Loki, CloudWatch) can provide searchable observability without requiring shell access.


Summary

Nuitka + Docker multi-stage builds produce containers that are:

  • 60–80% smaller than CPython equivalents
  • Faster to start (measured in milliseconds, not seconds)
  • Safer: no interpreter, no shell in distroless runtime
  • IP-protected: machine code, not decompilable bytecode

The only cost is longer CI build times. With ccache and BuildKit layer caching, this overhead is manageable — and a 5-minute build that produces a 40 MB secure container is a worthwhile trade against a 30-second build that ships a 900 MB image with exposed source code.


Related: Nuitka Packaging for Web Frameworks: FastAPI, Flask, and Django | Nuitka vs PyInstaller: Python Packaging Compared