diff --git a/.devcontainer/README.adoc b/.devcontainer/README.adoc
index edf06be..ed9db42 100644
--- a/.devcontainer/README.adoc
+++ b/.devcontainer/README.adoc
@@ -5,7 +5,7 @@
 
 == Overview
 
-This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, and just pre-installed. Dev container features add git, just, and nickel automatically.
+This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, and just pre-installed. Dev container features add git, just, nickel, and Julia 1.10 (via Juliaup) automatically.
 
 == VS Code (Local)
 
@@ -25,4 +25,4 @@ This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, an
 
 == Customization
 
-Replace `Statistikles` placeholders in both `devcontainer.json` and `Containerfile` with your actual project name. Run `just deps` to verify the environment after first launch.
+Run `just setup` to verify the environment after first launch (already run automatically by `postCreateCommand`).
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 31230a8..efb1683 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -18,10 +18,13 @@
       "version": "latest"
     },
     "ghcr.io/jdx/devcontainer-features/just:1": {},
-    "ghcr.io/nickel-lang/devcontainer-feature:0": {}
+    "ghcr.io/nickel-lang/devcontainer-feature:0": {},
+    "ghcr.io/julialang/devcontainer-features/julia:1": {
+      "channel": "1.10"
+    }
   },
 
-  "postCreateCommand": "just deps",
+  "postCreateCommand": "just setup",
 
   "remoteUser": "nonroot",
 
@@ -34,6 +37,7 @@
     "vscode": {
       "extensions": [
         "EditorConfig.EditorConfig",
+        "julialang.language-julia",
         "eamodio.gitlens",
         "streetsidesoftware.code-spell-checker",
         "timonwong.shellcheck",
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..7a46de5
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: MPL-2.0
+# Build-context excludes for `docker build`/`podman build` (Containerfile,
+# .devcontainer/Containerfile). Podman falls back to this file when no
+# .containerignore is present.
+
+.git/
+.github/
+.devcontainer/
+docs/
+proofs/
+ffi/
+benches/
+examples/
+generated/
+.claude/
+node_modules/
+*.log
+/tmp/
diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml
new file mode 100644
index 0000000..7f154c7
--- /dev/null
+++ b/.github/workflows/container-build.yml
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: MPL-2.0
+# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
+#
+# Statistikles — Container Build
+#
+# Builds the root Containerfile so it cannot silently rot back into a
+# template stub. Build only — no push, no registry login.
+
+name: Container Build
+
+on:
+  push:
+    branches: [main]
+    paths:
+      - 'Containerfile'
+      - '.dockerignore'
+      - 'Project.toml'
+      - 'Manifest.toml'
+      - 'src/**'
+      - '.github/workflows/container-build.yml'
+  pull_request:
+    branches: [main]
+    paths:
+      - 'Containerfile'
+      - '.dockerignore'
+      - 'Project.toml'
+      - 'Manifest.toml'
+      - 'src/**'
+      - '.github/workflows/container-build.yml'
+  workflow_dispatch:
+
+permissions: read-all
+
+concurrency:
+  group: container-build-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  build:
+    name: docker build .
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
+
+      - name: Build Containerfile
+        run: docker build -f Containerfile -t statistikles:ci .
+
+      - name: Smoke — run the entrypoint
+        run: |
+          # No LM Studio endpoint is reachable from the runner, so main()
+          # (src/tools/chat.jl) takes its documented offline-demo fallback
+          # and exits 0 — see the Containerfile's ENTRYPOINT comment and
+          # QUICKSTART-USER.adoc "First Run".
+          docker run --rm statistikles:ci | tee /tmp/container-run.log
+          grep -q "Statistikles v0.1.0" /tmp/container-run.log
+          grep -q "Cannot connect to LM Studio" /tmp/container-run.log
+          grep -q "All numbers above were computed by Julia, not an LLM." /tmp/container-run.log
diff --git a/Containerfile b/Containerfile
index 985834d..d9c0372 100644
--- a/Containerfile
+++ b/Containerfile
@@ -5,37 +5,39 @@
 # Build: podman build -t statistikles:latest -f Containerfile .
 # Run:   podman run --rm -it statistikles:latest
 # Seal:  selur seal statistikles:latest
-
-# --- Build stage ---
-FROM cgr.dev/chainguard/wolfi-base:latest AS build
-
-# TODO: Install build dependencies for your stack
-# Examples:
-#   RUN apk add --no-cache rust cargo       # Rust
-#   RUN apk add --no-cache elixir erlang    # Elixir
-#   RUN apk add --no-cache zig              # Zig
-
-WORKDIR /build
-COPY . .
-
-# TODO: Replace with your build command
-# Examples:
-#   RUN cargo build --release
-#   RUN mix deps.get && MIX_ENV=prod mix release
-#   RUN zig build -Doptimize=ReleaseSafe
-
-# --- Runtime stage ---
-FROM cgr.dev/chainguard/static:latest
-
-# Copy built artifact from build stage
-# TODO: Replace with your binary/artifact path
-# Examples:
-#   COPY --from=build /build/target/release/statistikles /usr/local/bin/
-#   COPY --from=build /build/_build/prod/rel/statistikles /app/
-#   COPY --from=build /build/zig-out/bin/statistikles /usr/local/bin/
-
-# Non-root user (chainguard images default to nonroot)
-USER nonroot
-
-# TODO: Replace with your entrypoint
-# ENTRYPOINT ["/usr/local/bin/statistikles"]
+#
+# Statistikles is a pure Julia package with no compiled binary artifact (see
+# "Build from Source" in QUICKSTART-MAINTAINER.adoc) — the `julia` executable
+# itself is the runtime, so there is nothing a separate build stage could
+# hand off that the runtime doesn't also need. A single stage on the official
+# Julia image is therefore correct here, not a shortcut. Chainguard ships no
+# Julia package, so the org-default Wolfi/static base (docs/AI-CONVENTIONS.md)
+# does not apply to this stack.
+
+# docker.io/library/julia:1.10 == Julia 1.10.11 (Debian trixie), matching the
+# `julia_version` pinned in the committed Manifest.toml and .tool-versions.
+FROM docker.io/library/julia:1.10@sha256:12477d07306333f0c59c04274196432a3834f8c44a0a10585de659fbf8ab0e54
+
+# Non-root runtime user
+RUN useradd --create-home --uid 1000 --shell /bin/bash statistikles
+WORKDIR /app
+COPY --chown=statistikles:statistikles . .
+USER statistikles
+ENV JULIA_DEPOT_PATH=/home/statistikles/.julia
+
+# Resolve the committed Manifest.toml exactly (no re-resolution) and
+# precompile so the entrypoint starts promptly.
+RUN julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()'
+
+# `main()` (src/tools/chat.jl) prints the startup banner, then tries to reach
+# an LM Studio-compatible endpoint (STATISTIKLES_LM_URL, default
+# http://localhost:1234/v1). With no LLM endpoint reachable from the
+# container it prints "Cannot connect..." and runs the offline
+# `run_examples()` demo instead, then exits 0 — it does not hang waiting for
+# input. Run with `--network=host` to reach a host-side LM Studio at the
+# default address for the interactive chat session instead. NOTE: setting
+# STATISTIKLES_LM_URL at `docker/podman run` time has no effect — Julia's
+# Pkg.precompile() above already baked the default URL into the package's
+# precompile cache; to change it, set STATISTIKLES_LM_URL as an image build
+# ENV before the RUN Pkg.precompile() line and rebuild.
+ENTRYPOINT ["julia", "--project=/app", "-e", "using Statistikles; main()"]
diff --git a/QUICKSTART-MAINTAINER.adoc b/QUICKSTART-MAINTAINER.adoc
index cfb8612..56157a8 100644
--- a/QUICKSTART-MAINTAINER.adoc
+++ b/QUICKSTART-MAINTAINER.adoc
@@ -45,9 +45,38 @@ the full Julia dependency closure from source — the closure is instantiated on
 from the pinned `Manifest.toml` into a writable depot on first use (see the
 header comment in `guix.scm` for the rationale and the exact command).
 
-The container (`Containerfile`) packaging surface exists in the repository but is
-not yet functional — do not base a container distribution on it until it is
-completed.
+The `Containerfile` also builds a runnable image (see "Container" below).
+
+== Container
+
+[source,bash]
+----
+podman build -t statistikles:latest -f Containerfile .
+podman run --rm -it statistikles:latest
+----
+
+(`docker` works identically in place of `podman`.) The image is built from
+the official `docker.io/library/julia:1.10` base (pinned by digest in the
+Containerfile), runs as a non-root user, and its `ENTRYPOINT` is the same
+`main()` entry point as `just run`. With no LLM endpoint reachable from the
+container it falls back to the offline `run_examples()` demo and exits 0 —
+see "First Run" in QUICKSTART-USER.adoc for the exact expected output. To
+reach a host-side LM Studio instance at the default address instead:
+
+[source,bash]
+----
+podman run --rm -it --network=host statistikles:latest
+----
+
+NOTE: `STATISTIKLES_LM_URL` set at `podman run` time has **no effect** on a
+precompiled image or a precompiled native install — verified by testing both.
+`Pkg.precompile()` evaluates and caches `BASE_URL` (`src/tools/lmstudio.jl`)
+into the package's precompile image at the time it runs; loading the package
+afterwards deserializes that cache rather than re-reading `ENV`. This is not
+container-specific — the same applies to `just build && just run` natively.
+To point a built image at a non-default endpoint, set `STATISTIKLES_LM_URL`
+as a build-time `ENV` *before* the `Pkg.precompile()` step in the
+Containerfile and rebuild.
 
 == Configuration
 
diff --git a/QUICKSTART-USER.adoc b/QUICKSTART-USER.adoc
index 87d3ba4..14c3599 100644
--- a/QUICKSTART-USER.adoc
+++ b/QUICKSTART-USER.adoc
@@ -37,6 +37,8 @@ Before you begin, ensure you have:
 
 == Install
 
+=== Option 1: Native Julia
+
 [source,bash]
 ----
 # Clone and set up
@@ -56,6 +58,23 @@ If you prefer not to use just, the equivalent raw command is:
 julia --project=. -e 'using Pkg; Pkg.instantiate()'
 ----
 
+=== Option 2: Container
+
+No local Julia install required — just Podman or Docker:
+
+[source,bash]
+----
+git clone https://github.com/hyperpolymath/statistikles.git
+cd statistikles
+podman build -t statistikles:latest -f Containerfile .
+podman run --rm -it statistikles:latest
+----
+
+(`docker` works identically in place of `podman`.) This builds the official
+`docker.io/library/julia:1.10` base image with the project instantiated and
+precompiled, then runs it as a non-root user. Expected output matches
+"First Run" below.
+
 == First Run
 
 [source,bash]
