Securing Dockerfile Python builds
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.