# Container Manager The Container Manager runs supervised container instances with full lifecycle control, from the web UI, the HTTP API, and Devii. Every instance runs ONE shared, prebuilt image; there is no per-project image building inside the app. A reconciling `BaseService` supervises the instances. > Audience: administrators. All run, exec, lifecycle, and schedule operations require an administrator, > because running containers with bind mounts and access to the docker socket is root-equivalent on the > host. ## Pieces - **Backend abstraction** (`services/containers/backend/`): a `Backend` ABC with a `DockerCliBackend` that drives the `docker` CLI via `asyncio.create_subprocess_exec`, streaming stdout line by line. A `FakeBackend` implements the same interface for tests with no daemon. The active backend is resolved by `runtime.get_backend()` and is the pluggable seam for a future Kubernetes or remote backend. - **Data model** (indexed in `database.py`): `instances`, `instance_events` (audit), `instance_metrics` (ring-buffered), `instance_schedules`. There are no dockerfile or build tables. - **ContainerService** (`service.py`): a reconciler. Each tick it snapshots `docker ps` (filtered by the `devplace.instance` label), converges every instance to its `desired_state`, applies restart policies, reaps orphan containers whose DB row is gone, fires due schedules, and samples metrics. The `devplace.instance=` label is the join key that guarantees no orphans and no lost state. Only the service lock owner reconciles; HTTP handlers only edit `desired_state`. ## The shared `ppy` image Every instance runs `ppy:latest` (override with the `DEVPLACE_CONTAINER_IMAGE` env var), built **once** with `make ppy` from `ppy.Dockerfile`. The image is a Python + Playwright base with a broad set of common Python libraries preinstalled, and it bakes in the security model: the `pravda` user at uid/gid `1000:1000`, a `sudo` superclone that never escalates (it runs commands as `pravda`, so a container can never write a root-owned file into the bind-mounted workspace), and the stdlib `pagent` agent at `/usr/bin/pagent.py`. Creating an instance is then an instant `docker run` (no build wait); it fails fast with a clear error if the `ppy` image has not been built yet. Projects that need a library not in the image install it at runtime with `pip install` (pravda owns the site-packages, so no `sudo` is needed), or add it to `ppy.Dockerfile` and rerun `make ppy`. ## Instances and lifecycle An instance is created with a name, optional boot command, env vars, CPU/memory limits, port maps, and restart policy. The project's files are materialized to a persistent host workspace and bind-mounted read-write as `/app`; the sync action imports container-side changes back into the project filesystem. Lifecycle operations (start, stop, restart, pause, resume, delete) flip the instance's `desired_state` and the reconciler executes them. Schedules use the shared cron/interval/one-time scheduler to start or stop instances. One-shot exec runs over HTTP; an interactive shell runs over a PTY-backed WebSocket on the lock owner. Each launch also injects per-instance `PRAVDA_*` env vars (`PRAVDA_BASE_URL`, `PRAVDA_OPENAI_URL`, `PRAVDA_API_KEY`, `PRAVDA_USER_UID`, `PRAVDA_CONTAINER_NAME`, `PRAVDA_CONTAINER_UID`) so code inside the container - and `pagent` - can call back into the platform and the AI gateway authenticated as the user. ## Runtime data and operations All generated data lives OUTSIDE the `devplacepy/` package and is never served via `/static`: persistent workspaces under `config.DATA_DIR/container_workspaces` (default `var/`, configurable with the `DEVPLACE_DATA_DIR` env var - point it at a volume in production). The docker daemon must be able to bind-mount `DATA_DIR` for the `/app` mount. The service is disabled by default (it needs the docker socket); an administrator enables **Containers** on `/admin/services`, and the `ppy` image must be built once with `make ppy`. ## Publishing a service (ingress) A running instance can be exposed by setting an `ingress_slug` plus an `ingress_port` (one of its mapped container ports). The `/p/` route (`routers/proxy.py`) then reverse-proxies both HTTP and WebSocket traffic to that container's published host port, stripping the `/p/` prefix. It is public but SSRF-safe: the target host and port are derived from the instance row, never from the request. So a container WebSocket server on port 8899 becomes reachable at `wss:///p//ws`. Set the **Public URL** in admin settings (`/admin/settings`) to your production origin; the container tools then return an absolute `ingress_url` (for example `https://your-host/p/`) so Devii and API clients use the real public address rather than localhost. ## Production The manager drives the host docker daemon, so production needs the opt-in overlay `docker-compose.containers.yml`. The `make docker-build` and `make docker-up` targets apply it automatically and self-derive its inputs, so it works with no `sudo`, no `/srv` dir, and no manual `.env` editing; a plain `docker compose up -d` drops the overlay and re-breaks it. The overlay mounts the docker socket (root on the host - trusted admins only), installs the docker CLI via the `INSTALL_DOCKER_CLI` build arg, and adds the host docker group (gid read from the socket via `stat -c '%g' /var/run/docker.sock`). Two docker-in-docker details matter: the data dir must be bind-mounted at the **same absolute path** on host and in the app container (make sets `DEVPLACE_DATA_DIR` to the project's own `./var`) because `docker run -v` resolves the source against the host; and the ingress proxy reaches published ports on the host via `DEVPLACE_CONTAINER_PROXY_HOST=host.docker.internal` (a bare-metal `make prod` deploy uses the default `127.0.0.1`). Remember to run `make ppy` on the production host too. Full steps are in the README "Container Manager wiring". ## Observability, CLI, and Devii The service tile on `/admin/services` shows instance counts; per-instance aggregates (CPU/memory, p95 runtime) are computed on read over the ring buffer. The CLI exposes `devplace containers list | reconcile | prune | prune-builds | gc-workspaces` (`prune-builds` is a one-time cleanup that removes legacy per-project images and the old dockerfiles/builds tables). Devii's admin `container_*` tools cover the same instance operations in natural language.