A simple CRM app used to demonstrate Coasts, a Docker-in-Docker isolated dev environment tool. The app has an Elixir/Phoenix API backend, a Next.js frontend, Postgres for storage, and Redis for session management.
coasts-demo/
backend/ Elixir/Phoenix API (containerized via Docker)
frontend/ Next.js app (runs as a bare process, not containerized)
test/ Integration tests (TypeScript, runs with tsx)
docker-compose.yml
Coastfile
docker compose up -d # starts backend, postgres, redis
cd frontend && npm install && npm run dev # starts Next.js on :3000The frontend proxies /api/* requests to localhost:4000 via Next.js rewrites.
coast build
coast run dev-1
coast checkout dev-1Open localhost:3000 (frontend) or localhost:4000/api/health (backend).
Straightforward — all services share one Docker bridge network:
Host machine
├── docker compose
│ ├── backend :4000 (Phoenix API)
│ ├── postgres :5432
│ └── redis :6379
└── frontend :3000 (bare process, Next.js dev server)
└── proxies /api/* → localhost:4000
Coasts runs the project inside a DinD container. The topology has three layers:
┌─── Host (macOS) ─────────────────────────────────────────────────┐
│ │
│ coast-dev daemon │
│ ├── socat forwarders (canonical ports) │
│ │ localhost:3000 ──→ DinD:3000 │
│ │ localhost:4000 ──→ DinD:4000 │
│ │ │
│ Host Docker daemon │
│ ├── coast-shared: postgres (:5432, on coast-shared-crm-demo) │
│ ├── coast-shared: redis (:6379, on coast-shared-crm-demo) │
│ │ │
│ ┌─── DinD Container (crm-demo-coasts-dev-1) ────────────────┐ │
│ │ │ │
│ │ Connected to: coast-shared-crm-demo network │ │
│ │ (can reach shared postgres/redis by hostname) │ │
│ │ │ │
│ │ Inner Docker daemon │ │
│ │ └── backend container :4000 │ │
│ │ ├── DATABASE_URL → postgres:5432 │ │
│ │ ├── REDIS_URL → redis:6379 │ │
│ │ └── extra_hosts: postgres → bridge gateway IP │ │
│ │ redis → bridge gateway IP │ │
│ │ │ │
│ │ /coast-supervisor/ (bare services) │ │
│ │ └── web (Next.js dev server) :3000 │ │
│ │ │ │
│ │ /workspace ← bind mount of project root │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
Key details:
-
Shared services (postgres, redis) run on the host Docker daemon, not inside DinD. They are shared across Coast instances and persist data between
coast rmcycles. Coast automatically strips them from the inner compose file and injectsextra_hostsentries so the backend container can reach them by hostname. -
Compose services (backend) run inside the inner Docker daemon within the DinD container. Coast builds the image on the host, loads it into the inner daemon, and starts it with a rewritten compose file that removes shared services and their
depends_onreferences. -
Bare services (frontend/web) run as plain processes directly on the DinD host OS, managed by
/coast-supervisor/. They are defined in[services.*]in the Coastfile and are not containerized. They have fast filesystem access to/workspace. -
Port forwarding uses
socaton the host.coast checkout dev-1binds canonical ports (3000, 4000, etc.) to the checked-out instance. Dynamic ports in the 49152-65535 range are always available for any running instance regardless of checkout state. -
Hot assign (
[assign.services] backend = "hot") means branch switches swap the/workspacebind mount without restarting containers. The backend picks up code changes via the mounted volume. Rebuild triggers (mix.exs,mix.lock,Dockerfile) force a full image rebuild when those files change.
# With backend running (locally or via Coast):
cd test && npx tsx integration.test.tsTests cover health checks, auth (register/login/logout), Redis-backed sessions, and contacts CRUD.