Containerize a Project#
How to build and run the container image that osprey build generates for
every project.
What You’ll Learn
What the generated
Dockerfile/.dockerignoreare and who owns themBuilding and running the image (ports, secrets, volumes)
The three build-arg extension points for site-specific installs
Path relocation with
osprey claude regen --runtime-rootAir-gapped images, the non-root requirement, and Kubernetes notes
Prerequisites: Docker (or Podman) installed; a project built with
osprey build.
Overview#
Every project built by osprey build includes a reference container
recipe at the project root:
Dockerfile— an image definition that installs the agent CLI and OSPREY, copies the project in, relocates its recorded paths, and serves the web terminal..dockerignore— keeps secrets (.env) and host-specific state (.venv,.git,_agent_data/) out of the image.
Both files are generated once and then yours: edit them freely, but keep
.dockerignore in place — the build depends on it, and it is what keeps
your .env secrets out of the image. osprey claude regen never touches
either file. To get a fresh copy, rebuild the project with
osprey build --force — note that this overwrites any edits you made to
the Dockerfile and .dockerignore (only .env, _agent_data/, and
.git survive a force rebuild).
Note
This page covers the project image — one container that runs the
assistant and its web terminal. osprey deploy manages the project’s
service containers (databases, MCP servers) — see Container Deployment
— but the two meet in one place: a deploy that includes the dispatch
worker builds this same project image (tagged <project>:local) for
the worker to run.
Quickstart#
cd my-project # the directory osprey build created
docker build -t my-project .
docker run --rm -p 8087:8087 --env-file .env my-project
Then open http://localhost:8087. Secrets are passed at runtime via
--env-file — the .dockerignore guarantees .env itself never
enters the image.
Build Arguments#
The image exposes these knobs for site-specific builds:
ARG |
Default |
Purpose |
|---|---|---|
|
|
pip requirement for OSPREY. Override with a |
|
|
Hosts exempted from any proxy during |
|
|
|
|
pinned at build time |
Version of the agent CLI installed into the image. The default pin matches the framework version that generated the Dockerfile; override to test a newer CLI without regenerating the project. |
(A fifth ARG, OSPREY_DEV, is used internally by osprey deploy up
--dev to install a locally built wheel; you normally never set it by hand.)
Example — install OSPREY from an internal mirror behind a proxy, with vendored assets for an air-gapped host:
docker build -t my-project \
--build-arg OSPREY_PIP_SPEC="git+https://git.example.gov/tools/osprey.git@main" \
--build-arg PIP_NO_PROXY="git.example.gov" \
--build-arg OSPREY_OFFLINE=1 .
Warning
Build-arg values persist in the image history (docker history).
Never put credentials in OSPREY_PIP_SPEC URLs for images you
distribute — prefer Docker build secrets or a credential-free
internal mirror.
Path Relocation#
A project built on a host records that host’s path in config.yml as
project_root. The generated Dockerfile fixes it during the image build:
RUN osprey claude regen --project /app/my-project --runtime-root /app/my-project
--runtime-root rewrites project_root in config.yml
(comment-preserving) and re-renders the agent artifacts (.mcp.json,
CLAUDE.md, .claude/) against the new root. No interpreter path is
recorded in config.yml, so nothing else needs relocating. This works for
projects built with or without osprey build --runtime-root.
Why Non-Root#
The image creates and switches to an unprivileged osprey user because
the agent CLI refuses to run in bypassPermissions mode as root. The CLI
itself is installed as a pinned global npm package, so it is runnable
by any user — keep the non-root user if you customize the recipe.
Runtime State and Volumes#
Two kinds of state are worth persisting across container restarts:
docker run --rm -p 8087:8087 --env-file .env \
-v my-project-agent-data:/app/my-project/_agent_data \
-v my-project-home:/home/osprey \
my-project
_agent_data/— executed scripts, user memory, API call logs./home/osprey— the agent CLI’s per-user state (sessions, credentials); setCLAUDE_CONFIG_DIRif you want it somewhere more explicit.
Kubernetes notes#
Give each user/instance a PVC for
/home/osprey(orCLAUDE_CONFIG_DIR) and one for_agent_data/— session state does not survive pod rescheduling otherwise.The container already runs as a non-root user, so a restricted
securityContext(runAsNonRoot: true) works out of the box.Expose port
8087(or override theCMDwith--port).
Troubleshooting#
pip fails building accelerator-toolbox on Apple Silicon — parts
of OSPREY’s dependency chain ship prebuilt wheels for linux/amd64 only,
so an arm64 build must compile them from source, which is where it breaks.
Build (and run) the amd64 image under Docker Desktop’s emulation instead; it
matches the usual amd64 deployment target:
docker build --platform linux/amd64 -t my-project .
docker run --platform linux/amd64 --rm -p 8087:8087 --env-file .env my-project
Customizing#
The file is yours — common edits:
Layer a site image on top: build the generated image as a base, then
FROMit in a small site Dockerfile that adds credentials helpers, enterprisemanaged-settings.json, or extra processes.Change the entrypoint: the default
CMDrunsosprey web --host 0.0.0.0 --port 8087 --project /app/<project>; override it to run a process supervisor if you add sidecars.Template-level override: a build profile’s app bundle can ship its own
apps/<bundle>/Dockerfile.j2, which takes precedence over the framework template at build time — use this when every project built from a bundle needs the same customization.
See also
- Container Deployment
Service containers (databases, MCP servers) via
osprey deploy— the complement to the project image on this page.- CLI Reference
osprey claude regen --runtime-rootandosprey vendorreference.