# Docker development environment

Rampway has one canonical root development image and one persistent Compose service. The image is digest-pinned Ubuntu 26.04 LTS with exact NodeSource Node 24, a checksummed signing key, the universal coding/debugging baseline, and the current OpenCode, Codex, Claude Code, and official Kimi Code CLIs. The build is source-independent: `.dockerignore` closes the build context to the Dockerfile metadata, the image copies no repository content, and project dependencies are installed only inside the running service.

## First use

Prerequisites are Docker with Compose v2 and a dedicated UID/GID-1000 development home containing this checkout at `$DEV_HOME_PATH/rampway`.

Copy the portable template and confirm its required external sources:

```bash
cp .env.example .env
test -d "${GH_CONFIG_SOURCE_PATH:-$HOME/.config/gh}"
test -d "${PROVIDER_RUNTIME_SOURCE_PATH:-/opt/hermes-dind-shared/auth/provider-runtime}"
```

Set `DEV_HOME_PATH` in `.env` to a dedicated home path containing this checkout at `$DEV_HOME_PATH/rampway`. Do not use an unrelated general host home or recursively change its ownership in the container. The environment template defaults to `/home/dev`; adjust it for any non-default host layout.

Run the preparation helper to create the required mount-target parent directories with correct permissions, then start the persistent service and install locked dependencies:

```bash
scripts/prepare-dev-home.sh
docker compose up --build --detach dev
scripts/docker-run.sh npm ci
```

`scripts/docker-run.sh` uses `docker compose exec -T dev` and preserves every command argument exactly. The service must already be running. Dependencies, caches, the checkout, and ordinary tool state persist naturally under the complete `/home/dev` bind.

## Isolation and mounts

Compose declares the default project name `rampway`. Override it with `COMPOSE_PROJECT_NAME` (or `-p`) and use a distinct `DEV_HOME_PATH` for each concurrent instance. Run `scripts/prepare-dev-home.sh` to create required mount-target parent directories before the first start:

```bash
COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
  scripts/prepare-dev-home.sh
COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
  docker compose up --build --detach dev
```

The `dev` service has exactly these mounts:

- `${DEV_HOME_PATH:?Set DEV_HOME_PATH in .env}` at `/home/dev`;
- `${GH_CONFIG_SOURCE_PATH:?Set GH_CONFIG_SOURCE_PATH in .env}` at `/home/dev/.config/gh`, read-only;
- `${PROVIDER_RUNTIME_SOURCE_PATH:-/opt/hermes-dind-shared/auth/provider-runtime}` at `/opt/hermes-dind-shared/auth/provider-runtime`.

The GitHub config directory is selected through `GH_CONFIG_DIR`. The one shared provider-runtime bind replaces individual provider mounts. The UID/GID-1000 bootstrap links `.codex`, `.local/share/opencode`, `.opencode`, and `.kimi-code` into the dedicated development home and fails closed on conflicting paths; it never copies credentials or changes ownership. The sole migration exception is a retained legacy `.codex` directory containing only a regular `auth.json`: bootstrap preserves it as `.codex.pre-provider-runtime` and installs the shared-runtime link, so exact resume remains safe and idempotent. SSH keys, Docker sockets, credentials in Compose values, source split mounts, named source/session volumes, fixed names/ports, privileged mode, sidecars, and container-side ownership fixups remain forbidden.

Provider CLI installation is not provider authorization. Threadwire remains parent-owned and is resolved outside this service through bare unversioned `npx threadwire`; Rampway installs no global or project Threadwire package and tracks no provider workspace profile.

## Static validation

The dependency-free verifier checks the image pin, NodeSource verification, provider specs, complete apt baseline, non-root identity, exact mounts, fail-closed interpolation, closed build context, forbidden topology, ignored `.env`, and helper contract. Its focused tests also inject representative regressions:

```bash
npm run verify:docker-dev-environment
npm run test:docker-dev-environment
```

## Docker-capable coordinator validation

The bootstrap service has no Docker access. A coordinator must render Compose and build the image cleanly:

```bash
export COMPOSE_PROJECT_NAME=rampway-canonical-validation
docker compose config --quiet
docker compose config
docker compose build --pull --no-cache dev
docker compose up --detach dev
docker compose exec -T dev sh -lc 'test "$HOME" = /home/dev && test "$PWD" = /home/dev/rampway && test "$(id -u)" = 1000 && test "$(id -g)" = 1000'
docker compose exec -T dev npm ci
docker compose exec -T dev npm run verify:docker-dev-environment
```

To prove two-instance isolation, prepare two UID/GID-1000 development homes with Rampway at `<home>/rampway`, start both, and compare their container IDs:

```bash
COMPOSE_PROJECT_NAME=rampway-isolation-a DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-a docker compose up --build --detach dev
COMPOSE_PROJECT_NAME=rampway-isolation-b DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-b docker compose up --build --detach dev
test "$(COMPOSE_PROJECT_NAME=rampway-isolation-a docker compose ps -q dev)" != "$(COMPOSE_PROJECT_NAME=rampway-isolation-b docker compose ps -q dev)"
```

For fresh cutover, provision a new dedicated home with Rampway already checked out at `<home>/rampway`, then perform a clean image build and dependency install:

```bash
export RAMPWAY_COMPOSE_PROJECT=rampway-cutover-fresh
export RAMPWAY_DEV_HOME=/srv/rampway-dev-homes/cutover-fresh
test -d "$RAMPWAY_DEV_HOME/rampway/.git"
COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose build --pull --no-cache dev
COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T dev npm ci
```

For exact resume, reuse the retained home and project name without reinstalling or relocating state, then prove the same checkout revision is visible:

```bash
test -n "$RAMPWAY_COMPOSE_PROJECT"
test -d "$RAMPWAY_DEV_HOME/rampway/.git"
export RAMPWAY_EXPECTED_HEAD="$(git -C "$RAMPWAY_DEV_HOME/rampway" rev-parse HEAD)"
COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T -e RAMPWAY_EXPECTED_HEAD="$RAMPWAY_EXPECTED_HEAD" dev sh -lc 'test "$PWD" = /home/dev/rampway && test "$(git rev-parse HEAD)" = "$RAMPWAY_EXPECTED_HEAD"'
```

The obsolete `/workspace` and named-volume state is not imported. Verify the new service before stopping the old project; removal of old named volumes is a separate destructive coordinator decision.
