Skip to content

Build a Python Application

Target audience: Developers Goal: Build a reproducible Python application using FROM stagex/pallet-python.

Prerequisites

Create the Project

We'll build dirtree — a recursive directory tree to JSON tool. It uses only the Python standard library.

mkdir -p dirtree/src && cd dirtree

src/dirtree.py

#!/usr/bin/env python3
import json
import os
import sys

def scan(path):
    name = os.path.basename(path.rstrip(os.sep)) or path
    if os.path.isdir(path):
        children = []
        try:
            for entry in sorted(os.listdir(path)):
                full = os.path.join(path, entry)
                children.append(scan(full))
        except PermissionError:
            children.append({"name": entry, "error": "permission denied"})
        return {"name": name, "type": "directory", "children": children}
    else:
        try:
            size = os.path.getsize(path)
        except OSError:
            size = 0
        return {"name": name, "type": "file", "size": size}

def main():
    root = sys.argv[1] if len(sys.argv) > 1 else "."
    print(json.dumps(scan(root), indent=2))

if __name__ == "__main__":
    main()

Write the Containerfile

Create Containerfile:

FROM docker.io/stagex/pallet-python@sha256:992287bf65a3eaa07f8b4082ad719f031bb26ec148f6696601ae5d5f17e1a747 AS build
WORKDIR /app
COPY src/ ./src/
RUN python -m py_compile src/dirtree.py && find . -name __pycache__ -exec rm -rf {} +

FROM docker.io/stagex/pallet-python@sha256:992287bf65a3eaa07f8b4082ad719f031bb26ec148f6696601ae5d5f17e1a747
COPY --from=build /app /app
ENTRYPOINT ["/usr/bin/python", "/app/src/dirtree.py"]

Key points:

  • FROM stagex/pallet-python (twice) — Python uses shared libraries (libpython3.13.so, OpenSSL, etc.), so FROM scratch won't work. Use the same pallet for both build and runtime. See the Rust guide for contrast with static binaries.
  • python -m py_compile — Syntax check gate. Catches errors at build time since Python has no compilation step.
  • No network needed — Unlike Rust's cargo fetch, Python's py_compile is purely local. No PyPI access required. For external dependencies, see Adding External Dependencies below.

Digest pinning: Always use @sha256: for reproducible builds. See the Quick Start to understand why.

Build

podman build --timestamp 1 -t dirtree .

Output:

[1/2] STEP 1/4: FROM docker.io/stagex/pallet-python@sha256:992...
[1/2] STEP 2/4: WORKDIR /app
[1/2] STEP 3/4: COPY src/ ./src/
[1/2] STEP 4/4: RUN python -m py_compile ...
[2/2] STEP 1/3: FROM docker.io/stagex/pallet-python@sha256:992...
[2/2] STEP 2/3: COPY --from=build /app /app
[2/2] COMMIT dirtree

Output:

[1/2] STEP 1/4: FROM docker.io/stagex/pallet-python@sha256:992...
[1/2] STEP 2/4: WORKDIR /app
[1/2] STEP 3/4: COPY src/ ./src/
[1/2] STEP 4/4: RUN python -m py_compile src/dirtree.py
[2/2] STEP 1/3: FROM docker.io/stagex/pallet-python@sha256:992...
[2/2] STEP 2/3: COPY --from=build /app /app
[2/2] COMMIT dirtree

Run

podman run --rm dirtree /app/src

Output:

{
  "name": "src",
  "type": "directory",
  "children": [
    {
      "name": "dirtree.py",
      "type": "file",
      "size": 835
    }
  ]
}

Adding External Dependencies

Pip is not available in StageX. All dependencies must be vendored manually.

The pattern: 1. Create a lib/ directory in your project 2. Copy .py files or pure-Python packages into lib/ 3. Set PYTHONPATH=/app/lib at runtime

Example: Vendor a Pure-Python Package

mkdir -p dirtree/lib
# Copy your pure-Python dependency files to lib/
# cp -r /path/to/mypackage dirtree/lib/

Updated Containerfile with Vendored Deps

FROM docker.io/stagex/pallet-python@sha256:992287bf65a3eaa07f8b4082ad719f031bb26ec148f6696601ae5d5f17e1a747 AS build
WORKDIR /app
COPY src/ ./src/
COPY lib/ ./lib/
    RUN python -m py_compile src/dirtree.py && find . -name __pycache__ -exec rm -rf {} +

    FROM docker.io/stagex/pallet-python@sha256:992287bf65a3eaa07f8b4082ad719f031bb26ec148f6696601ae5d5f17e1a747
    COPY --from=build /app /app
    ENV PYTHONPATH=/app/lib
ENTRYPOINT ["/usr/bin/python", "/app/src/dirtree.py"]

C extensions: Packages with C extensions must be built from source as StageX packages. See Add a New Package if you need to contribute Python packages with native code.

Verify Reproducibility

Unlike Rust's static binary (FROM scratch), Python's runtime image has a full filesystem with timestamps. To get reproducible image digests, use --timestamp 1 to normalize all file timestamps to epoch:

podman build --no-cache --timestamp 1 -t dirtree:rebuild .
podman inspect dirtree --format '{{.Digest}}'
podman inspect dirtree:rebuild --format '{{.Digest}}'

Both digests will be identical. See the Quick Start tutorial for a detailed walkthrough of why reproducibility matters.

See Also