When plain HTTP from 92 reaches the control plane but the WebSocket dial RSTs, test the upgrader on each side: 1. curl from 186 to 127.0.0.1:3452 with WS upgrade headers: - 101 → control plane is fine, network is the issue. - RST/4xx → control plane is broken. 2. curl from 92 to 186:3452 with WS upgrade headers: - 101 → firewall allows WS traffic, agent's client is the issue. - RST → some middlebox matches on the Upgrade header. - 4xx → control plane rejects the upgrade.
Sandbox Deployment Platform (SDP)
Internal deployment platform for Backend/QA. Lets a developer deploy a feature branch into an isolated sandbox, with the API Gateway routing selected services to the sandbox and the rest to OCP. See REQUIREMENTS.md for the full spec.
Status (Slice 2 — sandboxes, routes, real auth, all MVP features)
./scripts/build.sh produces three Linux/amd64 binaries and a static
dashboard. The full MVP flow works end to end:
- Real Bitbucket auth via
git ls-remoteagainst the api-gateway. - Real repo and branch listing via agent WS frames.
- Sandbox / template / environment CRUD with persisted metadata in SQLite.
- Route overrides per sandbox, with live read-back of the
<service>_urlmap from the gateway'sconfig.phpafter every branch switch. The agent patches the file and gracefully reloads apache. - Per-deploy port binding: the user picks the host port per service
(e.g. eredar at
172.18.136.92:9001), the container's exposed port is published to that port. - Erangel deploy:
git reset --hard → fetch → checkout → pull → composer install → start container → re-apply route overrides. Per-branch OCP-default snapshot persisted to<repo>/.sdp/ocp-defaults.json.
See REQUIREMENTS.md for the per-feature checklist.
Layout
.
├── protocol/ # shared wire types (Event, DeployRequest, RouteOverride, ...)
├── agentlib/ # Go. Shared agent library: gitutil + deployer (Go/PHP flavours)
├── control-plane/ # Go. HTTP API + WS hub + SQLite/.log persistence
├── agent-micro/ # Go. Runs on 172.18.136.92, deploys Go microservices
├── agent-gateway/ # Go. Runs on 172.18.139.186, deploys the PHP API Gateway
├── dashboard/ # NextJS static export, served by nginx
├── nginx/ # reference nginx config (manually applied on 186)
├── scripts/ # build, deploy, ssh wrappers
├── docker-compose.yml # all three services on alpine:latest
├── systemd/ # unit files for the three long-running services
├── go.work # Go workspace — one build, five modules
└── bin/ # built binaries (tracked, see .gitignore comment)
agentlib/ is a shared library used by both agents. It owns the git
helpers and the per-deployment state machine, which has two constructors
for two build flavours:
NewGo— for microservices. Runsgo buildon the host, thendocker run alpine:3.20with the host repo bind-mounted at/srcand the binary as the container command.alpine:3.20must be pre-loaded on the host (see Offline VMs).NewPHP— for the API Gateway (erangel). Runsgit reset --hard → fetch → checkout → pull → composer install (best-effort) → docker run php:8.3-apache, with the repo bind-mounted at/var/www/html/erangel-oceanandAPACHE_DOCUMENT_ROOT=/var/www/html/erangel-oceanso the gateway is served at/erangel/, mirroring production. After the container is up, the agent'sAfterStartcallback re-applies the active route overrides and reloads apache.php:8.3-apachemust be pre-loaded on the host. The agent is written in Go; the thing it deploys is a PHP project.
Prerequisites
- Docker (for the build container)
- Node 18+ (for the dashboard)
sshpass(for the deploy scripts:brew install sshpass)
No Go install needed locally — scripts/build.sh cross-compiles inside
golang:1.24-alpine.
Build
./scripts/build.sh
Outputs:
bin/control-plane,bin/agent-micro,bin/agent-gateway(Linux/amd64 ELF, statically linked)dashboard/out/(NextJS static export)
The build script:
- Starts a
golang:1.24-alpinecontainer with the repo bind-mounted. apk add git(the base image has none).- Configures
safe.directory /srcso the container's root user can read the bind-mounted host tree. - Cross-compiles all three binaries with
GOOS=linux GOARCH=amd64 CGO_ENABLED=0,-trimpath(reproducible builds) and-ldflags="-s -w"(strip debug info). chmod +xthe binaries inside the container (the host user can't chmod files written by the container's root).- Builds the Next.js dashboard with
npm install && npm run build.
The script verifies each binary with file to catch a missing
GOOS/GOARCH.
Deploy
./scripts/deploy.sh
This script:
- SSHs to 172.18.136.92 (
administrator) and pushesbin/agent-microplussystemd/sdp-agent-micro.serviceto the VM, then runssystemctl enable --now sdp-agent-micro. - SSHs to 172.18.139.186 (
administrator) and pushesbin/control-plane,bin/agent-gateway,dashboard/out/, and the matchingsystemd/*.servicefiles, then runssystemctl enable --nowfor both. The control plane is restarted first so the gateway agent's-cpURL has something to dial.
All three long-running services (control plane + both agents) are
plain host processes managed by systemd. The unit files live in
systemd/. Service containers spawned by the agents
(sdp-<repo>) are managed by docker, not systemd — the agents talk
to the host's dockerd via /var/run/docker.sock to create and
replace them.
Nginx on 186 is configured by hand; the dashboard ends up at
/home/administrator/SDP/dashboard/. The required location blocks
are in nginx/sandbox.conf (the actual deployment
on 186) and nginx/nginx.conf (a legacy
root-mount reference).
Override the creds via SDP_92_PASS / SDP_186_PASS env vars.
Local dev (docker compose)
For dev on a single host (e.g. a laptop with Docker):
./scripts/build.sh
docker compose up -d
Three services come up on alpine:latest:
control-plane→:3452(an unusual port to avoid collisions)agent-micro(connects to control plane, has docker socket + repos mounted)agent-gateway(same shape)
Architecture notes
- Pass-through creds. Bitbucket credentials travel with each deploy
request from control plane to agent, are used once for
git fetch/checkout/pull, and are never logged or persisted on the agent. - No Dockerfile build on the agent. Each agent does the language
build on the host (Go or composer), then
docker run <base-image>with the host repo bind-mounted and the binary / apache as the container command. The base image must be pre-loaded. - Offline VMs.
alpine:3.20andphp:8.3-apacheare pre-loaded viadocker load. The dashboard is a static export, no runtime fetches. - Persistence. Deployment progress goes to SQLite
(
<data>/sdp.db). Log lines go to append-only<data>/logs/<deploymentId>.log. SQLite usesmodernc.org/sqlite(pure Go, no cgo) so the control plane binary stays statically linkable. The driver name issqlite(notsqlite3). - Docker SDK. The agents use the official Moby Go SDK at
github.com/moby/moby/clientv0.5.0. - Realtime transport. WebSocket end-to-end. Agents connect to
/ws/agenton the control plane; the dashboard subscribes to/ws/deployments/{id}.
MVP stubs (intentional, deferred)
These are marked with ponytail: comments in the code and are
scheduled for later slices.
CheckOriginin the WS upgrader — open CORS, intentional for an internal tool.- "Drop on backpressure" policy for slow WS subscribers — replace with flow control or persistent event log if the dashboard ever needs catch-up replay.
- O(n) log tail scan in
store.TailLogs— fine for tail use; swap to a ring buffer if logs get huge.
Slice 2 dashboard
The dashboard has these pages:
/— login (real git-ls-remote via the gateway agent)./dashboard— quick deploy (ad-hoc single-service deploy)./dashboard/sandboxes— list, create, clone-from-template./dashboard/sandboxes/{id}— sandbox detail. Live routes from the gateway'sconfig.php, per-route toggle (OCP / sandbox override), microservice deploys with per-service host port and env./dashboard/templates— template CRUD./dashboard/environments— env CRUD./dashboard/history— deployment history (filterable by sandbox).
See also
- REQUIREMENTS.md — full spec, infra, MVP success criteria, per-feature status checklist
- nginx/nginx.conf — reference nginx config
- docker-compose.yml — three-service dev stack