The control plane binary will create the data dir on first start, but doing it before systemd starts the service means the ReadWritePaths scope has somewhere to point at, and faster diagnosis if anything else is wrong.
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