Build a Python Application
Target audience: Developers Goal: Build a reproducible Python application using
FROM stagex/pallet-python.
Prerequisites
- Completed the Quick Start tutorial
- Podman installed
- Basic familiarity with Python
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.), soFROM scratchwon'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'spy_compileis 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
- Quick Start tutorial — prerequisite
- Build a Rust Application how-to — contrasts static binary vs shared-library approach
- Containerfile Syntax reference — syntax details
- Add a New Package how-to — if you want to contribute Python packages to StageX
- Reproduce Builds how-to — verification beyond basic digest check