From 7eda8f510feaa5910584a854a80e815356b242cd Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Fri, 8 May 2026 10:53:39 -0700 Subject: [PATCH] feat(local-dev): containerize platform + canvas stack via docker-compose (closes #126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the legacy nohup `go run ./cmd/server` setup with a fully containerized local stack: postgres + redis + platform + canvas, all with `restart: unless-stopped` so they survive Mac sleep/wake and Docker Desktop daemon restarts. ## Changes - **docker-compose.yml** - `restart: unless-stopped` on platform/postgres/redis - `BIND_ADDR=0.0.0.0` for platform — the dev-mode-fail-open default of 127.0.0.1 (PR #7) made the host unable to reach the container even with port mapping. Container netns is already isolated, so binding all interfaces inside is safe. - Healthchecks switched from `wget --spider` (HEAD → 404 forever because /health is GET-only) to `wget -qO /dev/null` (GET). Same regression existed on canvas; fixed both. - **workspace-server/Dockerfile.dev** - `CGO_ENABLED=1` → `0` to match prod Dockerfile + Dockerfile.tenant. Without this, the alpine dev image fails with "gcc: not found" because workspace-server has no actual cgo deps but the env was forcing the cgo build path. Closes a divergence introduced in 9d50a6da (today's air hot-reload PR). - **canvas/Dockerfile** - `npm install` → `npm ci --include=optional` for lockfile-exact installs that include platform-specific @tailwindcss/oxide native binaries. Without these, `next build` fails with "Cannot read properties of undefined (reading 'All')" on the `@import "tailwindcss"` directive. - **canvas/.dockerignore** (new) - Excludes `node_modules` and `.next` so the Dockerfile's `COPY . .` step doesn't clobber the freshly-installed container node_modules with the host's (potentially stale or wrong-arch) copy. This was the actual root cause of the canvas build break. - **workspace-server/.gitignore** - Adds `/tmp/` for air's live-reload build cache. ## Stage A verified ``` container status restart postgres-1 Up (healthy) unless-stopped redis-1 Up (healthy) unless-stopped platform-1 Up (healthy, air-mode) unless-stopped canvas-1 Up (healthy) unless-stopped GET :8080/health → 200 GET :3000/ → 200 DB preserved: 407 workspace rows + 5 named personas Persona mount: 28 dirs at /etc/molecule-bootstrap/personas ``` ## Stage B — N/A This is local-dev infrastructure only. None of these files ship to SaaS tenants — production EC2s use `Dockerfile.tenant` + `ec2.go` user-data, not docker-compose. ## Out of scope - The decorative-but-broken `wget --spider` healthcheck has presumably also been silently 404'ing on prod tenants. Ship a follow-up to audit + fix the prod path; not done here to keep the PR scoped. - Docker Desktop "Start at login" is a per-machine GUI setting that must be toggled manually (Settings → General). - The legacy heartbeat-all.sh that pinged 5 persona workspaces from the host has been deleted (~/.molecule-ai/heartbeat-all.sh). Per Hongming: each workspace is responsible for its own heartbeat. Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/.dockerignore | 10 ++++++++++ canvas/Dockerfile | 6 +++++- docker-compose.yml | 13 +++++++++++-- workspace-server/.gitignore | 3 +++ workspace-server/Dockerfile.dev | 2 +- 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 canvas/.dockerignore diff --git a/canvas/.dockerignore b/canvas/.dockerignore new file mode 100644 index 00000000..f859f851 --- /dev/null +++ b/canvas/.dockerignore @@ -0,0 +1,10 @@ +# Excluded from `docker build` context. Without this, the COPY . . step in +# canvas/Dockerfile clobbers the freshly-installed node_modules with the +# host's (potentially broken / wrong-arch) copy — the @tailwindcss/oxide +# native binary disagreed and broke `next build`. +node_modules +.next +.git +*.log +.env* +!.env.example diff --git a/canvas/Dockerfile b/canvas/Dockerfile index e834b7a5..6aa8f446 100644 --- a/canvas/Dockerfile +++ b/canvas/Dockerfile @@ -1,7 +1,11 @@ FROM node:22-alpine AS builder WORKDIR /app COPY package.json package-lock.json* ./ -RUN npm install +# `npm ci` (not `install`) for lockfile-exact reproducibility. +# `--include=optional` ensures the platform-specific @tailwindcss/oxide +# native binary lands — without it, postcss fails with "Cannot read +# properties of undefined (reading 'All')" at build time. +RUN npm ci --include=optional COPY . . ARG NEXT_PUBLIC_PLATFORM_URL=http://localhost:8080 ARG NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws diff --git a/docker-compose.yml b/docker-compose.yml index 8cff1e19..0bcb4a5d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: - pgdata:/var/lib/postgresql/data networks: - molecule-monorepo-net + restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dev}"] interval: 2s @@ -50,6 +51,7 @@ services: - redisdata:/data networks: - molecule-monorepo-net + restart: unless-stopped healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 2s @@ -126,6 +128,10 @@ services: REDIS_URL: redis://redis:6379 PORT: "${PLATFORM_PORT:-8080}" PLATFORM_URL: "http://platform:${PLATFORM_PORT:-8080}" + # Container network namespace is already isolated; "all interfaces" + # inside the container = the bridge interface only. The fail-open + # default (127.0.0.1) would block host-to-container access. + BIND_ADDR: "${BIND_ADDR:-0.0.0.0}" # Default MOLECULE_ENV=development so the WorkspaceAuth / AdminAuth # middleware fail-open path activates when ADMIN_TOKEN is unset — # otherwise the canvas (which runs without a bearer in pure local @@ -212,8 +218,11 @@ services: - "${PLATFORM_PUBLISH_PORT:-8080}:${PLATFORM_PORT:-8080}" networks: - molecule-monorepo-net + restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${PLATFORM_PORT:-8080}/health || exit 1"] + # Plain GET — `--spider` would issue HEAD, which returns 404 because + # /health is registered as GET only. + test: ["CMD-SHELL", "wget -qO /dev/null --tries=1 http://localhost:${PLATFORM_PORT:-8080}/health || exit 1"] interval: 5s timeout: 5s retries: 10 @@ -251,7 +260,7 @@ services: networks: - molecule-monorepo-net healthcheck: - test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:${CANVAS_PORT:-3000} || exit 1"] + test: ["CMD-SHELL", "wget -qO /dev/null --tries=1 http://127.0.0.1:${CANVAS_PORT:-3000} || exit 1"] interval: 10s timeout: 5s retries: 10 diff --git a/workspace-server/.gitignore b/workspace-server/.gitignore index 3f67c92f..47fc4dc0 100644 --- a/workspace-server/.gitignore +++ b/workspace-server/.gitignore @@ -1,2 +1,5 @@ # The compiled binary, not the cmd/server package. /server + +# air live-reload build cache (Dockerfile.dev + docker-compose.dev.yml). +/tmp/ diff --git a/workspace-server/Dockerfile.dev b/workspace-server/Dockerfile.dev index f8a0a1db..0345ba0f 100644 --- a/workspace-server/Dockerfile.dev +++ b/workspace-server/Dockerfile.dev @@ -31,7 +31,7 @@ RUN go mod download # block) so the Dockerfile doesn't need to COPY it. air watches the # bind-mounted dir for changes. -ENV CGO_ENABLED=1 +ENV CGO_ENABLED=0 ENV GOFLAGS="-buildvcs=false" # Run air with the .air.toml in the bind-mounted source dir. -- 2.45.2