# Packaging and Deploying to the Metacircular Platform This guide provides everything needed to build, package, and deploy a service to the Metacircular platform. It assumes no prior knowledge of the platform's internals. --- ## Platform Overview Metacircular is a multi-service infrastructure platform. Services are Go binaries running as containers on Linux nodes, managed by these core components: | Component | Role | |-----------|------| | **MCP** (Control Plane) | Deploys, monitors, and manages container lifecycle via rootless Podman | | **MCR** (Container Registry) | OCI container registry at `mcr.svc.mcp.metacircular.net:8443` | | **mc-proxy** (TLS Proxy) | Routes traffic to services via L4 (SNI passthrough) or L7 (TLS termination) | | **MCIAS** (Identity Service) | Central SSO/IAM — all services authenticate through it | | **MCNS** (DNS) | Authoritative DNS for `*.svc.mcp.metacircular.net` | The operator workflow is: **build image → push to MCR → write service definition → deploy via MCP**. MCP handles port assignment, route registration, and container lifecycle. --- ## Prerequisites | Requirement | Details | |-------------|---------| | Go | 1.25+ | | Container engine | Docker or Podman (for building images) | | `mcp` CLI | Installed on the operator workstation | | MCR access | Credentials to push images to `mcr.svc.mcp.metacircular.net:8443` | | MCP agent | Running on the target node (currently `rift`) | | MCIAS account | For `mcp` CLI authentication to the agent | --- ## 1. Build the Container Image ### Dockerfile Pattern All services use a two-stage Alpine build. This is the standard template: ```dockerfile FROM golang:1.25-alpine AS builder RUN apk add --no-cache git WORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . ARG VERSION=dev RUN CGO_ENABLED=0 go build -trimpath \ -ldflags="-s -w -X main.version=${VERSION}" \ -o / ./cmd/ FROM alpine:3.21 RUN apk add --no-cache ca-certificates tzdata COPY --from=builder / /usr/local/bin/ WORKDIR /srv/ EXPOSE ENTRYPOINT [""] CMD ["server", "--config", "/srv//.toml"] ``` ### Dockerfile Rules - **`CGO_ENABLED=0`** — all builds are statically linked. No CGo in production. - **`ca-certificates` and `tzdata`** — required in the runtime image for TLS verification and timezone-aware logging. - **No `USER` directive** — containers run as `--user 0:0` under MCP's rootless Podman. UID 0 inside the container maps to the unprivileged `mcp` host user. A non-root `USER` directive creates a subordinate UID that cannot access host-mounted volumes. - **No `VOLUME` directive** — causes layer unpacking failures under rootless Podman. The host volume mount is declared in the service definition, not the image. - **No `adduser`/`addgroup`** — unnecessary given the rootless Podman model. - **`WORKDIR /srv/`** — so relative paths resolve correctly against the mounted data directory. - **Version injection** — pass the git tag via `--build-arg VERSION=...` so the binary can report its version. - **Stripped binaries** — `-trimpath -ldflags="-s -w"` removes debug symbols and build paths. ### Split Binaries If the service has separate API and web UI binaries, create separate Dockerfiles: - `Dockerfile.api` — builds the API/gRPC server - `Dockerfile.web` — builds the web UI server Both follow the same template. The web binary communicates with the API server via gRPC (no direct database access). ### Makefile Target Every service includes a `make docker` target: ```makefile docker: docker build --build-arg VERSION=$(shell git describe --tags --always --dirty) \ -t -f Dockerfile.api . ``` --- ## 2. Write a Service Definition Service definitions are TOML files that tell MCP what to deploy. They live at `~/.config/mcp/services/.toml` on the operator workstation. ### Minimal Example (Single Component, L7) ```toml name = "myservice" node = "rift" [build.images] myservice = "Dockerfile" [[components]] name = "web" image = "mcr.svc.mcp.metacircular.net:8443/myservice:v1.0.0" [[components.routes]] port = 8443 mode = "l7" ``` ### API Service Example (L4, Multiple Routes) ```toml name = "myservice" node = "rift" [build.images] myservice = "Dockerfile" [[components]] name = "api" image = "mcr.svc.mcp.metacircular.net:8443/myservice:v1.0.0" volumes = ["/srv/myservice:/srv/myservice"] cmd = ["server", "--config", "/srv/myservice/myservice.toml"] [[components.routes]] name = "rest" port = 8443 mode = "l4" [[components.routes]] name = "grpc" port = 9443 mode = "l4" ``` ### Full Example (API + Web) ```toml name = "myservice" node = "rift" [build.images] myservice = "Dockerfile.api" myservice-web = "Dockerfile.web" [[components]] name = "api" image = "mcr.svc.mcp.metacircular.net:8443/myservice:v1.0.0" volumes = ["/srv/myservice:/srv/myservice"] cmd = ["server", "--config", "/srv/myservice/myservice.toml"] [[components.routes]] name = "rest" port = 8443 mode = "l4" [[components.routes]] name = "grpc" port = 9443 mode = "l4" [[components]] name = "web" image = "mcr.svc.mcp.metacircular.net:8443/myservice-web:v1.0.0" volumes = ["/srv/myservice:/srv/myservice"] cmd = ["server", "--config", "/srv/myservice/myservice.toml"] [[components.routes]] port = 443 mode = "l7" ``` ### Conventions A few fields are derived by the agent at deploy time: | Field | Default | Override when... | |-------|---------|------------------| | Source path | `` relative to workspace root | Directory name differs from service name (use `path`) | | Hostname | `.svc.mcp.metacircular.net` | Service needs a public hostname (use route `hostname`) | All other fields must be explicit in the service definition. ### Service Definition Reference **Top-level fields:** | Field | Required | Purpose | |-------|----------|---------| | `name` | Yes | Service name (matches project name) | | `node` | Yes | Target node to deploy to | | `active` | No | Whether MCP keeps this running (default: `true`) | | `path` | No | Source directory relative to workspace (default: `name`) | **Build fields:** | Field | Purpose | |-------|---------| | `build.images.` | Maps build image name to Dockerfile path. The `` must match the repository name in a component's `image` field (the part after the last `/`, before the `:` tag). | **Component fields:** | Field | Required | Purpose | |-------|----------|---------| | `name` | Yes | Component name (e.g. `api`, `web`) | | `image` | Yes | Full image reference (e.g. `mcr.svc.mcp.metacircular.net:8443/myservice:v1.0.0`) | | `volumes` | No | Volume mounts (list of `host:container` strings) | | `cmd` | No | Command override (list of strings) | | `env` | No | Extra environment variables (list of `KEY=VALUE` strings) | | `network` | No | Container network (default: none) | | `user` | No | Container user (e.g. `0:0`) | | `restart` | No | Restart policy (e.g. `unless-stopped`) | **Route fields (under `[[components.routes]]`):** | Field | Purpose | |-------|---------| | `name` | Route name — determines `$PORT_` env var | | `port` | External port on mc-proxy (e.g. `8443`, `9443`, `443`) | | `mode` | `l4` (TLS passthrough) or `l7` (TLS termination by mc-proxy) | | `hostname` | Public hostname override | ### Routing Modes | Mode | TLS handled by | Use when... | |------|----------------|-------------| | `l4` | The service itself | Service manages its own TLS (API servers, gRPC) | | `l7` | mc-proxy | mc-proxy terminates TLS and proxies HTTP to the service (web UIs) | ### Version Pinning Component `image` fields **must** pin an explicit semver tag (e.g. `mcr.svc.mcp.metacircular.net:8443/myservice:v1.1.0`). Never use `:latest`. This ensures deployments are reproducible and `mcp status` shows the actual running version. The version is extracted from the image tag. --- ## 3. Build, Push, and Deploy ### Tag the Release ```bash git tag -a v1.0.0 -m "v1.0.0" git push origin v1.0.0 ``` ### Build and Push Images ```bash mcp build ``` This reads the `[build.images]` section of the service definition, builds each Dockerfile, tags the images with the version from the definition, and pushes them to MCR. The workspace root is configured in `~/.config/mcp/mcp.toml`: ```toml [build] workspace = "~/src/metacircular" ``` Each service's source is at `/` (where `path` defaults to the service name). ### Sync and Deploy ```bash # Push all service definitions to agents, auto-build missing images mcp sync # Deploy (or redeploy) a specific service mcp deploy ``` `mcp sync` checks whether each component's image tag exists in MCR. If missing and the source tree is available, it builds and pushes automatically. `mcp deploy` pulls the image on the target node and creates or recreates the containers. ### What Happens During Deploy 1. Agent assigns a free host port (10000–60000) for each declared route. 2. Agent starts containers with `$PORT` / `$PORT_` environment variables set to the assigned ports. 3. Agent registers routes with mc-proxy (hostname → `127.0.0.1:`, mode, TLS cert paths). 4. Agent records the full state in its SQLite registry. On stop (`mcp stop `), the agent reverses the process: removes mc-proxy routes, then stops containers. --- ## 4. Data Directory Convention All runtime data lives in `/srv//` on the host. This directory is bind-mounted into the container. ``` /srv// ├── .toml # Configuration file ├── .db # SQLite database (created on first run) ├── certs/ # TLS certificates │ ├── cert.pem │ └── key.pem └── backups/ # Database snapshots ``` This directory must exist on the target node before the first deploy, owned by the `mcp` user (which runs rootless Podman). Create it with: ```bash sudo mkdir -p /srv//certs sudo chown -R mcp:mcp /srv/ ``` Place the service's TOML configuration and TLS certificates here before deploying. --- ## 5. Configuration Services use TOML configuration with environment variable overrides. ### Standard Config Sections ```toml [server] listen_addr = ":8443" grpc_addr = ":9443" tls_cert = "/srv//certs/cert.pem" tls_key = "/srv//certs/key.pem" [database] path = "/srv//.db" [mcias] server_url = "https://mcias.metacircular.net:8443" ca_cert = "" service_name = "" tags = [] [log] level = "info" ``` For services with a web UI, add: ```toml [web] listen_addr = "127.0.0.1:8080" vault_grpc = "127.0.0.1:9443" vault_ca_cert = "" ``` ### $PORT Convention When deployed via MCP, the agent assigns host ports and passes them as environment variables. **Applications should not hardcode listen addresses** — they will be overridden at deploy time. | Env var | When set | |---------|----------| | `$PORT` | Component has a single unnamed route | | `$PORT_` | Component has named routes | Route names are uppercased: `name = "rest"` → `$PORT_REST`, `name = "grpc"` → `$PORT_GRPC`. **Container listen address:** Services must bind to `0.0.0.0:$PORT` (or `:$PORT`), not `localhost:$PORT`. Podman port-forwards go through the container's network namespace — binding to `localhost` inside the container makes the port unreachable from outside. Services built with **mcdsl v1.1.0+** handle this automatically — `config.Load` checks `$PORT` → overrides `Server.ListenAddr`, and `$PORT_GRPC` → overrides `Server.GRPCAddr`. These take precedence over TOML values. Services not using mcdsl must check these environment variables in their own config loading. ### Environment Variable Overrides Beyond `$PORT`, services support `$SERVICENAME_SECTION_KEY` overrides. For example, `$MCR_SERVER_LISTEN_ADDR=:9999` overrides `[server] listen_addr` in MCR's config. `$PORT` takes precedence over these. --- ## 6. Authentication (MCIAS Integration) Every service delegates authentication to MCIAS. No service maintains its own user database. ### Auth Flow 1. Client sends credentials to the service's `POST /v1/auth/login`. 2. Service forwards them to MCIAS via the client library (`git.wntrmute.dev/mc/mcias/clients/go`). 3. MCIAS validates and returns a bearer token. 4. Subsequent requests include `Authorization: Bearer `. 5. Service validates tokens via MCIAS `ValidateToken()`, cached for 30s (keyed by SHA-256 of the token). ### Roles | Role | Access | |------|--------| | `admin` | Full access, policy bypass | | `user` | Access governed by policy rules, default deny | | `guest` | Service-dependent restrictions, default deny | Admin detection comes solely from the MCIAS `admin` role. Services never promote users locally. --- ## 7. Networking ### Hostnames Every service gets `.svc.mcp.metacircular.net` automatically. Public-facing services can declare additional hostnames: ```toml [[components.routes]] port = 443 mode = "l7" hostname = "docs.metacircular.net" ``` ### TLS - **Minimum TLS 1.3.** No exceptions. - L4 services manage their own TLS — certificates go in `/srv//certs/`. - L7 services have TLS terminated by mc-proxy — certs are stored at `/srv/mc-proxy/certs/.pem`. - Certificate and key paths are required config — the service refuses to start without them. ### Container Networking Containers join the `mcpnet` Podman network by default. Services communicate with each other over this network or via loopback (when co-located on the same node). --- ## 8. Command Reference | Command | Purpose | |---------|---------| | `mcp build ` | Build and push images to MCR | | `mcp sync` | Push all service definitions to agents; auto-build missing images | | `mcp deploy ` | Pull image, (re)create containers, register routes | | `mcp undeploy ` | Full teardown: remove routes, DNS, certs, and containers | | `mcp stop ` | Remove routes, stop containers | | `mcp start ` | Start previously stopped containers | | `mcp restart ` | Restart containers in place | | `mcp ps` | List all managed containers and status | | `mcp status [service]` | Detailed status for a specific service | --- ## 9. Complete Walkthrough Deploying a new service called `myservice` from scratch: ```bash # 1. Prepare the target node ssh rift sudo mkdir -p /srv/myservice/certs sudo chown -R mcp:mcp /srv/myservice # Place myservice.toml and TLS certs in /srv/myservice/ exit # 2. Tag the release cd ~/src/metacircular/myservice git tag -a v1.0.0 -m "v1.0.0" git push origin v1.0.0 # 3. Write the service definition cat > ~/.config/mcp/services/myservice.toml << 'EOF' name = "myservice" node = "rift" [build.images] myservice = "Dockerfile.api" [[components]] name = "api" image = "mcr.svc.mcp.metacircular.net:8443/myservice:v1.0.0" volumes = ["/srv/myservice:/srv/myservice"] [[components.routes]] name = "rest" port = 8443 mode = "l4" [[components.routes]] name = "grpc" port = 9443 mode = "l4" EOF # 4. Build and push the image mcp build myservice # 5. Deploy mcp deploy myservice # 6. Verify mcp status myservice mcp ps ``` The service is now running, with mc-proxy routing `myservice.svc.mcp.metacircular.net` traffic to the agent-assigned ports. --- ## Appendix: Repository Layout Services follow a standard directory structure: ``` . ├── cmd// CLI entry point (server, subcommands) ├── cmd/-web/ Web UI entry point (if separate) ├── internal/ All service logic (not importable externally) │ ├── auth/ MCIAS integration │ ├── config/ TOML config loading │ ├── db/ Database setup, migrations │ ├── server/ REST API server │ ├── grpcserver/ gRPC server │ └── webserver/ Web UI server (if applicable) ├── proto//v1/ Protobuf definitions ├── gen//v1/ Generated gRPC code ├── web/ Templates and static assets (embedded) ├── deploy/ │ ├── -rift.toml Reference MCP service definition │ ├── docker/ Docker Compose files │ ├── examples/ Example config files │ └── systemd/ systemd units ├── Dockerfile.api API server container ├── Dockerfile.web Web UI container (if applicable) ├── Makefile Standard build targets └── .toml.example Example configuration ``` ### Standard Makefile Targets | Target | Purpose | |--------|---------| | `make all` | vet → lint → test → build (the CI pipeline) | | `make build` | `go build ./...` | | `make test` | `go test ./...` | | `make vet` | `go vet ./...` | | `make lint` | `golangci-lint run ./...` | | `make docker` | Build the container image | | `make proto` | Regenerate gRPC code from .proto files | | `make devserver` | Build and run locally against `srv/` config | --- ## Appendix: Currently Deployed Services For reference, these services are operational on the platform: | Service | Version | Node | Purpose | |---------|---------|------|---------| | MCIAS | v1.8.0 | (separate) | Identity and access | | Metacrypt | v1.1.0 | rift | Cryptographic service, PKI/CA | | MC-Proxy | v1.2.1 | rift | TLS proxy and router | | MCR | v1.2.0 | rift | Container registry | | MCNS | v1.1.0 | rift | Authoritative DNS | | MCDoc | v0.1.0 | rift | Documentation server | | MCP | v0.4.0 | rift | Control plane agent |