Young's Mind
Decisions

Docker systems design — applying multi-tenant hosting to our production node

Young, 2026-10-13: "Min study with Tam for Docker systems design. How to apply to our current production server."

Docker systems design — applying multi-tenant hosting to our production node

Young, 2026-10-13: "Min study with Tam for Docker systems design. How to apply to our current production server."

Tam's A2A is wedged (confirmed), so this design is vault-routed. Everything below was measured live on 161.33.71.67 on 2026-10-13 — nothing assumed.


1. The node, measured

SpecMeasuredYoung's brief
Archaarch64 (Neoverse-N1)ARM ✅
CPU44 vCPU ✅
RAM23 GiB total, 21 GiB available24 GB ✅ (close)
Disk193 GB, 179 GB free (14 G used)200 GB ✅
Docker29.7.2, Compose v5.4.0, active— (already installed)
Containers runningnone — clean slate—

Docker is already there. That removes the first deployment risk entirely.

Current residents (must not break)

ServicePortNotes
nginx80, 443reverse proxy + TLS termination
solos-docs.service127.0.0.1:8091docs.youngle.au
pulse-site.service127.0.0.1:8092pulse.youngle.au
sol.service (node)127.0.0.1:4090sol.youngle.au
yukonit site127.0.0.1:8012yukonit.youngle.au
tailscaled—tailnet: solos, lab (offline 13d), pulse

Resident footprint: ~1.5 GiB RAM, ~14 GB disk. Available for tenants: ~20 GiB RAM, ~179 GB disk.

🔴 Finding: Tailscale is already a mesh

100.124.146.127  solos   (this node)
100.94.188.127   lab     offline, last seen 13d ago
100.85.164.30    pulse   active, direct

This changes the A2A answer. The earlier problem — LAB's :9900 bound to 127.0.0.1, unreachable from pulse — has a cleaner fix than binding to the LAN: bind to the Tailscale interface (100.x) instead of 0.0.0.0. Same isolation benefit (only tailnet members reach it), no LAN exposure, no firewall rule needed. Sol's card should carry this option.


2. Tenancy design — the core decision

Per-tenant Docker Compose project, on a per-tenant bridge network, with NO host port publication. nginx proxies to container IPs on the tenant network.

Why this beats host-port allocation:

ApproachCollision riskIsolationComplexity
Host ports per tenant (9001, 9002…)🔴 high — must track a global mapweaklow
Per-tenant network + container-name proxy✅ none — each network has its own IP spacestrongmedium
Single shared cluster🔴 tenant blast radiusnonehigh

The design:

nginx (host, 80/443)
  │  proxy_pass http://medusa-<tenant>:9000   (container DNS on the tenant network)
  ▼
docker network: tenant-<slug>  (bridge, isolated)
  ├── postgres:15-alpine      volume: pg-<slug>
  ├── redis:7-alpine          volume: redis-<slug>
  ├── medusa-server           WORKER_MODE=server,  DISABLE_MEDUSA_ADMIN=false
  ├── medusa-worker           WORKER_MODE=worker,  DISABLE_MEDUSA_ADMIN=true
  ├── storefront              Next.js
  └── eve-agent               Nitro (self-hosted)

nginx connects to tenant networks (docker network connect) and proxies by container name — so no tenant publishes a host port at all. Port collisions become structurally impossible, not a checklist item.

Resource ceilings (prevent one tenant starving the node)

Every container gets explicit limits — the node has 4 CPUs shared with four existing services, and one runaway tenant must not take the others down:

deploy:
  resources:
    limits:   { cpus: "1.0", memory: 2G }   # medusa-server
    # worker 0.5/1G · storefront 0.5/1G · pg 0.5/0.7G · redis 0.25/0.1G · eve 0.25/0.3G

Per-tenant ceiling ≈ 3.0 CPU / 5.1 GiB — 4 tenants fit the measured budget with headroom. This matches Tam's 4.8 GB/stack estimate.


3. The four constraints that shape everything

3.1 Builds are the real CPU risk, not runtime

medusa build and Next.js builds spike hard on 4 shared vCPUs. Tam's study already named the fix — build once per template release, one shared image for all tenants. Concretely: a CI/build step produces solos-medusa:<tag>, pushed to a local registry container on the node; tenants pull it. Never build per-tenant.

3.2 Disk is the long-term ceiling, not RAM

179 GB free, but each tenant carries a Postgres volume + backups + logs. Budget: ~2 GB image layers (shared) + ~1 GB/tenant volumes + backups. Backups go offsite (not on this node) — the node cannot be both the host and the backup target.

3.3 Existing services must not be disturbed

nginx config is shared. Tenant vhosts are added as new files in sites-enabled/, never by editing the existing four. TLS via the existing letsencrypt pattern. A tenant onboarding that touches docs.youngle.au or pulse.youngle.au is a defect.

3.4 Tailscale is the private plane

Tenant admin panels should not be publicly exposed by default. Bind admin to the tailnet interface, public storefront only through nginx. This is free security we already own.


4. Onboarding — one script, probe-gated

Per Tam's study, and matching our probe discipline:

1.  slug=<tenant>            # validate: lowercase, no collisions
2.  generate secrets         # openssl rand -hex 32 × 4 (COOKIE, JWT, DB, REDIS)
                             # → ~/.solos-tenants/<slug>/secrets.env, chmod 0600
3.  render compose           # template → docker-compose.<slug>.yml (per-tenant names)
4.  docker network create tenant-<slug>
5.  docker compose -f ... up -d
6.  run predeploy            # medusa db:migrate + link sync
7.  create admin user        # npx medusa user -e … -p …  (generated, stored 0600)
8.  nginx vhost + certbot
9.  GATE: curl /health → OK  AND  /eve/v1/health → OK  AND  admin loads
10. only then: mark tenant live

Nothing is "live" until step 9 passes. Same rule as our Heart worker: a capability with no probe is not a deliverable.

Updates

git tag → pull template → predeploy (auto-migrate) → restart → health re-check
Rollback = prior tag + prior volumes (never deleted on update)

5. What this means for Sol's lane (feature specs)

The Docker design constrains what Sol can spec:

  1. Every new feature must be per-tenant configurable — no global state. A feature that writes to a shared table breaks the isolation model.
  2. Admin features bind to tailnet, not public — any admin surface he specs needs a network-scope decision.
  3. POS payments — if it needs a terminal service, it runs as another container on the tenant network (no host port).
  4. Resource limits are part of the spec — a feature that needs >1 CPU steady breaks the 4-tenant budget.

6. What this means for Mira's lane (finance)

The design gives her the real numbers:

  • COGS per tenant ≈ 0 marginal on the free tier, but the node caps at ~4 tenants
  • 5th tenant = new node (paid VPS or second ARM) — that's the step-function in the cost model
  • Disk/backup is the long-term ceiling, so backup storage is a real line item
  • The tailnet and nginx are already paid for — shared infra, no per-tenant cost

7. Honest gaps in this design

  • Not built. This is a design on measured facts, not a running tenant.
  • No tenant has been deployed — the onboarding script is a spec, not a tested script.
  • Backup/restore not designed beyond "goes offsite" — needs a real restore drill (a backup never restored is not a backup).
  • The local registry container is proposed, not built.
  • EVE health endpoint (/eve/v1/health) is from Tam's study, not probed by me.
  • Tailscale A2A fix is a recommendation, not implemented — Sol's lane.
Copyright © 2026