Container Deployment#

How to run an Osprey project’s containerized services with osprey deploy.

What You’ll Learn
  • What osprey deploy does and when you need it

  • Configuring services in config.yml (minimal example)

  • Authoring docker-compose.yml.j2 templates

  • Network binding, .env loading, and the --dev workflow

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:

  1. Resolve the project directory and load config.yml via ConfigBuilder.

  2. Set deployment.bind_address (127.0.0.1 by default, 0.0.0.0 with --expose).

  3. Render the root services/docker-compose.yml.j2 (shared osprey-network).

  4. For each entry in deployed_services: clean and create the build dir, render the service compose template, copy service files.

  5. If copy_src: true, copy src/ into the build as repo_src/, plus requirements.txt and pyproject.toml (renamed pyproject_user.toml).

  6. With --dev, build a wheel from the local Osprey checkout and drop it into the build dir.

  7. Copy any additional_dirs into the build.

  8. Auto-create _agent_data/ subdirectories declared under file_paths.

  9. Write a flattened config.yml per service. ${VAR} placeholders are preserved (secrets stay out of the rendered output and are resolved at container start).

  10. 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 advisoryosprey deploy up and osprey deploy status compare the project’s recorded provenance (osprey version and a content hash of the resolved preset, stamped into .osprey-manifest.json at build time) against the installed framework, and warn when the render is out of date — up prints the exact rebuild command, status a general reminder. The warning never blocks a deploy; projects built before the hash existed get the version comparison only.

  • Endpoint summary — every osprey deploy up ends with a summary of the published service endpoints, including an explicit web 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

OSPREY_POSTGRES_IMAGE

services.postgresql.image

openobserve

OSPREY_OPENOBSERVE_IMAGE

services.openobserve.image

event_dispatcher

OSPREY_DISPATCH_IMAGE

services.event_dispatcher.image

dispatch_worker

OSPREY_WORKER_IMAGE

services.dispatch_worker.image

nextcloud_bridge

OSPREY_NEXTCLOUD_BRIDGE_IMAGE

services.nextcloud_bridge.image

bluesky

OSPREY_BLUESKY_BRIDGE_IMAGE

services.bluesky.image

bluesky (Tiled sidecar)

OSPREY_TILED_IMAGE

services.bluesky.tiled_image

bluesky_panels

OSPREY_BLUESKY_PANELS_IMAGE

services.bluesky_panels.image

virtual_accelerator

OSPREY_VA_IMAGE

services.virtual_accelerator.image

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 deploy command 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.