Skip to content
← Blog

Technical explainer

Securing Dockerfile Python builds

2 min read

A basic secure Python Dockerfile uses multi-stage builds, --require-hashes, and a non-root user. But there are additional layers that significantly improve the security profile of containerized Python applications.

The baseline secure Dockerfile

Start here if you haven't already:

# Stage 1: Build
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --require-hashes --no-cache-dir --prefix=/install -r requirements.txt

# Stage 2: Runtime
FROM python:3.11-slim
# Non-root user
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /home/appuser/app
COPY --from=builder /install /usr/local
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]

This baseline: pins and hash-verifies all packages, separates build and runtime stages, runs as a non-root user.

Layer 1: Immutable base image

Always pin your base image to a specific digest, not just a tag:

# Pinned to a tag — the image content may change
FROM python:3.11-slim

# Pinned to a digest — the image content cannot change
FROM python:3.11-slim@sha256:d3af7985a5a...

Tags like 3.11-slim can be reassigned to point to a different image. A SHA-256 digest is immutable. Rebuilding with the same Dockerfile will always produce the same base layer.

To get the digest for a tag:

docker pull python:3.11-slim
docker inspect python:3.11-slim --format='{{index .RepoDigests 0}}'

Layer 2: Read-only filesystem

For applications that don't need to write to the filesystem at runtime:

# In docker-compose.yml or deployment config
services:
  app:
    image: myapp:latest
    read_only: true
    tmpfs:
      - /tmp  # if /tmp is needed

Or with Docker run:

docker run --read-only --tmpfs /tmp myapp:latest

A malicious package that establishes persistence by writing to the filesystem can't do so if the filesystem is read-only. The process can still run, but any write operations outside /tmp will fail.

Layer 3: BuildKit secrets for private packages

If your build needs to install from a private PyPI index, avoid passing credentials as environment variables:

# Insecure — credentials baked into image layers
ARG PYPI_TOKEN
RUN pip install --extra-index-url https://user:${PYPI_TOKEN}@private.pypi.example.com/ mypackage

Use BuildKit secrets instead:

# syntax=docker/dockerfile:1
FROM python:3.11-slim AS builder

# Mount the secret during the RUN command only
# The secret never appears in image layers
RUN --mount=type=secret,id=pip_config,dst=/etc/pip.conf \
    pip install --no-cache-dir -r requirements.txt

Build with:

docker buildx build --secret id=pip_config,src=./pip.conf .

The pip.conf file (containing the private index URL and credentials) is available to the pip install command but is never stored in any image layer.

Layer 4: No-new-privileges and seccomp

# Prevent privilege escalation (setuid, setgid binaries)
docker run --security-opt no-new-privileges myapp:latest

# Apply a seccomp profile that limits available syscalls
docker run --security-opt seccomp=./seccomp-profile.json myapp:latest

A default seccomp profile blocks ~44 syscalls, including most that malware uses to establish persistence or escalate privileges. The Python default seccomp profile is available at https://github.com/docker/docker/blob/master/profiles/seccomp/default.json.

Layer 5: Scan after build

Even with all the above, run a vulnerability scan on your built images before pushing to production:

# Trivy — open source vulnerability scanner
trivy image myapp:latest

# Or in CI with GitHub Actions
- uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:latest
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

Layer 6: Veln in the build CI

Pair everything above with Veln in your CI pipeline:

- uses: veln-sh/setup-action@v1
  with:
    license-key: ${{ secrets.VELN_LICENSE_KEY }}
    mode: enforce
- run: docker build -t myapp:latest .

When Docker build runs pip install, Veln Gate intercepts the traffic. Any BLOCK or WARN verdict fails the build before the image is created.

Putting it all together

# syntax=docker/dockerfile:1
FROM python:3.11-slim@sha256:d3af7985a5a... AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=secret,id=pip_config,dst=/etc/pip.conf \
    pip install --require-hashes --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.11-slim@sha256:d3af7985a5a...
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /home/appuser/app
COPY --from=builder /install /usr/local
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]

Deploy with:

docker run --read-only --tmpfs /tmp \
    --security-opt no-new-privileges \
    --security-opt seccomp=./seccomp-profile.json \
    myapp:latest

Each layer adds defense in depth. You don't need all of them on day one — start with the baseline and add layers as your security posture matures.


Veln adds supply chain verification to your Docker build pipeline at the install step.