Container Deployment#
How to run an Osprey project’s containerized services with osprey deploy.
What You’ll Learn
What
osprey deploydoes and when you need itConfiguring services in
config.yml(minimal example)Authoring
docker-compose.yml.j2templatesNetwork binding,
.envloading, and the--devworkflow
Prerequisites: Docker or Podman installed locally.
Tip
This page is the operator/service-author reference for osprey deploy.
For the end-to-end build → ship workflow (CI/CD, release operations), use
the osprey-build-deploy skill that the build interview installs into
your profile repo. For the full services: schema as authored inside a
build profile, see Services.
Overview#
osprey deploy renders each service’s Jinja2 Docker Compose template,
copies source and configuration into a per-service build directory, and
hands the result to Docker or Podman Compose. A new project built from the
control-assistant preset deploys a full stack out of the box:
postgresql, openobserve, event_dispatcher and dispatch_worker,
bluesky (with its co-deployed Tiled data server), virtual_accelerator,
bluesky_panels, and the multi-user web-terminal stack. Even the minimal
hello-world preset deploys one service (openobserve, for telemetry).
You only need this page when you add or customize a containerized service.
Service Configuration#
Services are declared under services: in config.yml and selected for
deployment via deployed_services:. A minimal example (one of the services
the control-assistant preset ships with):
services:
postgresql:
path: ./services/postgresql
database_name: ariel
username: ariel
password: ariel
port_host: 5432
deployed_services:
- postgresql
Each service entry must point path: at a directory containing a
docker-compose.yml.j2 template. Everything else under the service key is
project-specific configuration exposed to the template as
{{services.<name>.<key>}}. Beyond path, a service entry may also
declare copy_src, additional_dirs, and render_kernel_templates
(a multi-container service is expressed by the docker-compose.yml.j2
template defining more than one compose service). For how facility services
are declared inside a build profile, see Services.
Service lookup namespaces#
A name in deployed_services is looked up by its literal spelling — there
is no search order. A plain name like postgresql resolves to top-level
services.postgresql. A dotted name picks its namespace explicitly:
osprey.<name> reads osprey.services.<name>, and
applications.<app>.<name> reads applications.<app>.services.<name>.
The flat form shown above is the common case; the namespaced forms exist for
build profiles that ship multiple applications.
CLI Commands#
osprey deploy up [-d|--detached] # Start services
osprey deploy down # Stop services
osprey deploy restart # Stop then start services
osprey deploy status # Show status table
osprey deploy build # Render compose files without starting
osprey deploy clean # Remove containers, volumes, and images (destructive)
osprey deploy rebuild # Clean, rebuild, and restart services
osprey deploy seed [USER] # (Re)seed multi-user web-terminal workspaces
osprey deploy decommission USER # Remove one user's workspace (--archive | --purge)
osprey deploy prune # Remove workspaces of users no longer in the index
# (--archive | --purge, --dry-run)
osprey deploy nuke # Tear down the whole multi-user stack (destructive)
Full command and flag reference: CLI Reference. Note there
is no osprey deploy logs subcommand — use docker logs <name> or
podman logs <name> directly.
The project directory is resolved as: --project flag, then
OSPREY_PROJECT environment variable, then current working directory.
Container Runtime Selection#
The runtime is auto-detected: if Docker’s daemon is reachable it is
preferred, otherwise Podman is used. Force a specific runtime with the
CONTAINER_RUNTIME environment variable or by setting
container_runtime: docker|podman|auto at the root of config.yml.
Deployment Workflow#
When osprey deploy up runs:
Resolve the project directory and load
config.ymlviaConfigBuilder.Set
deployment.bind_address(127.0.0.1by default,0.0.0.0with--expose).Render the root
services/docker-compose.yml.j2(sharedosprey-network).For each entry in
deployed_services: clean and create the build dir, render the service compose template, copy service files.If
copy_src: true, copysrc/into the build asrepo_src/, plusrequirements.txtandpyproject.toml(renamedpyproject_user.toml).With
--dev, build a wheel from the local Osprey checkout and drop it into the build dir.Copy any
additional_dirsinto the build.Auto-create
_agent_data/subdirectories declared underfile_paths.Write a flattened
config.ymlper service.${VAR}placeholders are preserved (secrets stay out of the rendered output and are resolved at container start).Shell out to
docker compose/podman compose.
Keeping a Rendered Project Up to Date#
A project directory is a rendered artifact: osprey build writes its
config.yml and service scaffolding from the preset at build time, and
osprey deploy up deploys exactly what that rendered config describes. If
the framework or preset gains features after the render, the stale project
still deploys “successfully” — just without them.
To update an existing project, re-run the build with --force:
osprey build my-project --preset my-preset --force
cd my-project && osprey deploy up -d
--force re-renders everything framework-owned and preserves what you
own: .env values (secrets, and the service tokens/passwords your
existing docker volumes were initialized with), _agent_data/, and the
project’s .git history. data/ is re-materialized from the preset.
Avoid guarding the build behind a directory-existence check
([ -d my-project ] || osprey build ...) — “exists” is not “current”,
and the guard silently skips exactly the re-render that an updated preset
needs.
Two guards make render drift visible:
Staleness advisory —
osprey deploy upandosprey deploy statuscompare the project’s recorded provenance (osprey version and a content hash of the resolved preset, stamped into.osprey-manifest.jsonat build time) against the installed framework, and warn when the render is out of date —upprints the exact rebuild command,statusa general reminder. The warning never blocks a deploy; projects built before the hash existed get the version comparison only.Endpoint summary — every
osprey deploy upends with a summary of the published service endpoints, including an explicitweb terminal (not configured in this project)line when the config declares no web tier, so a missing service is a stated fact rather than a silent absence.
Docker Compose Templates#
Each service needs a docker-compose.yml.j2 template in its service
directory. In addition, a root-level services/docker-compose.yml.j2
is required to define the shared network (osprey-network). Without it,
deploy build and deploy up will fail.
services/
├── docker-compose.yml.j2 # Required: shared network definition
└── postgresql/
└── docker-compose.yml.j2 # Per-service template
Per-service templates have access to the full configuration plus a few engine-injected values:
# services/postgresql/docker-compose.yml.j2
services:
postgresql:
container_name: {{services.postgresql.container_name | default('osprey-postgres')}}
labels:
osprey.project.name: "{{osprey_labels.project_name}}"
osprey.project.root: "{{osprey_labels.project_root}}"
osprey.deployed.at: "{{osprey_labels.deployed_at}}"
ports:
- "{{deployment.bind_address}}:{{services.postgresql.port_host}}:5432"
environment:
TZ: {{system.timezone}}
networks:
- osprey-network
Common access patterns: {{services.<name>.<key>}},
{{file_paths.<key>}}, {{system.<key>}}, {{project_root}},
{{deployment.bind_address}}, and {{osprey_labels.project_name}} /
project_root / deployed_at (injected by the deploy engine).
Service Template Ownership#
The service templates under <project>/services/ are framework-managed:
every osprey build refreshes them from the installed OSPREY version, so
compose fixes reach your project automatically. Do not edit them in place —
your changes would be overwritten on the next build.
To customize a service template, claim it first:
osprey scaffold claim services/postgresql # freeze for local editing
osprey scaffold diff services/postgresql # compare against the framework
osprey scaffold unclaim services/postgresql # restore framework management
A claimed service is recorded in config.yml under scaffold.user_owned
and skipped by every subsequent build; osprey scaffold diff shows how far
your copy has drifted from the current framework template. This is the same
ownership mechanism used for the Claude Code artifacts (osprey scaffold
list shows both).
Before reaching for a claim, check whether a config key or environment variable already covers your need — most service knobs (ports, images, credentials, retention) are configurable without forking the template.
Overriding Service Images#
Every service image resolves through the same three-layer chain — an
environment variable wins, then a config.yml key, then the packaged
default:
Service |
Environment variable |
Config key |
|---|---|---|
postgresql |
|
|
openobserve |
|
|
event_dispatcher |
|
|
dispatch_worker |
|
|
nextcloud_bridge |
|
|
bluesky |
|
|
bluesky (Tiled sidecar) |
|
|
bluesky_panels |
|
|
virtual_accelerator |
|
|
Point either layer at an internal registry mirror or a pinned digest when your deployment host cannot (or should not) pull public images.
Network Binding and Security#
Services bind to 127.0.0.1 by default. Use --expose only when you
have authentication and firewalling in place — --expose overrides any
deployment.bind_address you set in config.yml.
Container networking uses service names as hostnames (e.g.,
postgresql:5432). For host access from inside containers, use
host.docker.internal (Docker) or host.containers.internal (Podman).
Environment Variables (.env)#
The deploy system passes a .env file from the project root to Docker /
Podman Compose via --env-file. Compose uses these values to fill in
${VAR} placeholders in the rendered compose files; a variable reaches a
running container only where a template maps it in.
cp .env.example .env
# Edit .env with your actual values
osprey deploy up also writes to this file: on first deploy it mints any
missing service tokens and passwords (for example EVENT_DISPATCHER_TOKEN,
ZO_ROOT_USER_PASSWORD, or ARIEL_DB_PASSWORD) so services never start
with blank or publicly-known credentials, and restricts the file to owner-only
permissions. Treat .env as the project’s secret store and keep it out of
version control.
Note
Postgres reads ARIEL_DB_PASSWORD (as POSTGRES_PASSWORD) only when
initializing a fresh data volume. A volume created before the password
was minted keeps its original password; the ${ARIEL_DB_PASSWORD:-ariel}
fallback in the shipped configs keeps such deployments working. To adopt
the minted password, remove the ariel_postgres_data volume and redeploy
(this deletes the stored logbook data — re-ingest afterwards).
If no .env file is found, services start with default/empty environment
variables and a warning is logged.
Development Mode#
The --dev flag deploys with your locally installed Osprey source
instead of the PyPI version:
osprey deploy up --dev
The system builds a wheel from your local Osprey source, copies it into each service’s build directory, and rebuilds the service images with that wheel installed — the images are built first, then started as-is. Your dev source is baked into the image at build time; nothing changes inside an already-running container. If the local source cannot be found (e.g., Osprey was installed from PyPI rather than editable mode), containers fall back to the PyPI version.
--dev requires the Python build package:
uv pip install build # or: pip install build
Troubleshooting#
Services fail to start: Check logs (docker logs <name> or
podman logs <name>), verify config.yml syntax, ensure .env
variables are set, confirm service paths contain docker-compose.yml.j2.
Port conflicts: lsof -i :<port> to find the culprit; update
port_host.
Template errors: Verify Jinja2 syntax ({{var}} not {var});
inspect rendered files under build/services/<name>/.
Daemon not running: Both Docker and Podman print platform-specific
hints; on macOS, start Docker Desktop or run podman machine start.
``–dev`` issues: Confirm the Osprey wheel (.whl) exists in the
service build directory, and that the image was rebuilt after your source
change — rerun osprey deploy up --dev to rebuild it.
See also
- CLI Reference
Full
osprey deploycommand and flag reference.- Services
Authoritative
services:schema for build profiles.- Containerize a Project
The project image (assistant + web terminal in one container) built from the generated
Dockerfile— distinct from the service containers this page covers.