containers Intermediate

Pyvorin in Docker: Best Practices

Compile in the builder stage, ship the disk cache in the image, and activate at container start. The Docker practices that matter for Pyvorin Native 1.0.9.

Published Jun 26, 2026

A Pyvorin Native 1.0.9 container image has exactly one job: carry a warm compile cache and a correctly activated runtime. Everything else about the Dockerfile follows from those two. This page covers the practices that hold up in production — platform pins, multi-stage compilation, layer ordering around the cache directory, and the licensing rule that trips most teams on first contact: activate at container start, never at image build. The facts underneath each practice are verified against the installed package; where Docker itself supplies the mechanism rather than Pyvorin, the page says so. None of it is exotic. All of it earns its place.

Pin the platform before anything else

Two pins decide whether the image builds at all. The first is the Python version: pyvorin-native 1.0.9 ships a single binary wheel for CPython 3.12, so the base image must be a 3.12 tag — python:3.12-slim is the usual choice. The second is the CPU platform: the wheel is x86_64-only, with no ARM build in this release, so the image target must be linux/amd64. This matters most on Apple Silicon developer machines, where the Docker default platform is linux/arm64 and the build fails at the install step with a platform error that sends people looking in the wrong place:

docker build --platform=linux/amd64 -t app:pyvorin .

Pin the Pyvorin version in requirements.txt as well, with an exact version rather than a compatible range. The compiled cache is keyed partly by the runtime's modification times, so a floating resolve that changes the package between build stages invalidates the cache the image was supposed to carry. Three pins — Python, platform, package — and the build becomes reproducible.

Multi-stage: compile in the builder, ship the cache

The multi-stage pattern compiles the application in a builder stage and copies the populated cache into the runtime image, so the running container holds the artefacts without carrying the build context:

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
# Compile as the last step, on byte-identical source
RUN python -m pyvorin compile app/pipeline.py --fail-on-fallback \
 && python -m pyvorin run app/pipeline.py --function run --compare --runs 3

FROM python:3.12-slim
WORKDIR /srv
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# The compiled cache, produced by the SAME package version and flags
COPY --from=builder /usr/local/lib/python3.12/site-packages/.pyvorin_cache \
                    /usr/local/lib/python3.12/site-packages/.pyvorin_cache
COPY app/ ./app/
# Activate at container start — see the licensing section
ENTRYPOINT ["sh", "-c", "pyvorin check || pyvorin activate \"$PYVORIN_KEY\"; exec python -m app.pipeline"]

Two details carry the correctness of the whole scheme. The --fail-on-fallback flag fails the image build if any function declines to compile, so an image that would run at interpreter speed never reaches a registry. And the cache copy takes .pyvorin_cache from the builder's site-packages into the runtime stage's site-packages at the same relative path — the cache's default location inside the install tree, <site-packages>/.pyvorin_cache/disk_compile, keyed by a hash of source, function names, compiler options and runtime modification times. Because both stages install from the same pinned requirements.txt, the package build matches and the keys hit. The deeper mechanics of shipping the warm cache are on the warm-up and caching strategies page and the CI artefact caching page.

Layer order: dependencies below, cache above, source at the top

The cache key explains the layer ordering. Because entries hash the application source, any source change invalidates every entry derived from it; because they hash the runtime's modification times, a package change invalidates them too. Order the layers so that what changes rarely sits below what changes often:

  1. Base image pin (the Python and platform pins).
  2. requirements.txt and dependency install — changes on dependency upgrades only.
  3. The copied .pyvorin_cache — changes when dependencies or compiled source change.
  4. Application source — changes on every commit, and deliberately last.

Place the source copy above the cache copy and a one-line edit recompiles nothing — the cache entries were built from identical source, so they still hit. The reverse order, with source below the cache, forces a cache copy on every commit and, worse, tempts builders to recompile inside the runtime stage where the verification flags no longer run. Keep the compile in the builder, the cache in the image, and the source on top. Add the local cache to .dockerignore so a developer's warm .pyvorin_cache never leaks into the build context and silently overrides the verified one. The same reasoning applies to the builder stage itself: pin its base image digest alongside the package pins, or a refreshed base can move the runtime's modification times and age the cache out from under you.

Activate at runtime, never at build time

This is the rule that conflicts with instinct, and it exists because of device binding. Activation computes a device fingerprint from hostname, username and machine identity; a licence activated during docker build is bound to the build host or the throwaway build container, and an image pushed to a registry carries a licence tied to an identity no other host shares. It will not activate on the machines that matter, and it has leaked the key into an image layer, where it stays visible to anyone who pulls the image. Activation belongs to the running container:

# At container start — key arrives from the environment or a secret mount
pyvorin activate "$PYVORIN_KEY"   # one POST: key + device fingerprint
pyvorin check                     # hard gate; exits non-zero if unlicensed

Supply PYVORIN_KEY through your orchestrator's secret mechanism, never through ENV instructions in the Dockerfile — image layers are not a secrets store, and every ENV line is recoverable from the published image. The single activation call reaches api.pyvorin.com with the key and fingerprint only, and after that a signed lease validates locally within the verified offline grace windows — 24 hours on demo and trial tiers, 72 hours on basic and professional, 90 days on enterprise. The full semantics are on the activation and entitlement page, and the complete endpoint list on the network requirements page. One honest caveat for short-lived containers: because the fingerprint binds to container identity, each container is its own licensed device, so batch-style workloads that spin up thousands of ephemeral containers should activate per scheduling unit or revisit the topology — a handful of long-lived workers serving many jobs amortise the licence far better than a swarm of seconds-long tasks. If the workload genuinely is thousands of disposable containers a day, say so in the design review and price the licences accordingly, or move the compiled work behind a stable worker pool.

Verify the image, then trust the fallback

Two safety nets finish the build. The first is verification: the --compare run in the builder stage checks native output against CPython on the same inputs, so a miscompiled artefact fails the build; pyvorin check at start confirms the licence gate before the application runs. The second is the runtime fallback: a function that declines to compile — say a dependency update introduced an unsupported construct — runs as its original Python and is recorded, so the container degrades to interpreter speed rather than crashing its workload. How much that matters depends on the service, and that is a judgement call, not a checkbox: for a CPU-bound batch transform, silent fallback quietly erases the reason the image exists, which is why --fail-on-fallback belongs in the build and why the fallback log should be scraped and alerted in production. For an I/O-bound service, compilation was never the point and the fallback is irrelevant. Decide which service you are building before deciding how loudly to treat a decline. The fallback mechanics are documented on the unsupported code and fallback page.

Where to go next

Last reviewed 26 June 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal (verified build of 12 September 2026). Platform pins reflect the shipped cp312-linux_x86_64 wheel; the Dockerfile practices are Docker mechanisms applied to verified package facts. Grace windows, fingerprint binding and endpoint lists are read from the installed licensing build.