From 7255bba8905dbe003a89382c265e30c9ec831b19 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 25 Mar 2026 22:03:36 -0700 Subject: [PATCH] Add deployment artifacts and rift config (Phase 13) Dockerfiles for API server and web UI (multi-stage, alpine:3.21, non-root mcr user). systemd units with security hardening. Idempotent install script. Rift-specific config with MCIAS service token, TLS paths, and Docker compose with loopback port bindings for mc-proxy fronting (28443/29443/28080). Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile.api | 37 ++++++++++++++++++ Dockerfile.web | 33 ++++++++++++++++ PROGRESS.md | 29 ++++++++++++-- deploy/docker/docker-compose-rift.yml | 46 ++++++++++++++++++++++ deploy/mcr-rift.toml | 37 ++++++++++++++++++ deploy/scripts/install.sh | 55 +++++++++++++++++++++++++++ deploy/systemd/mcr-backup.service | 25 ++++++++++++ deploy/systemd/mcr-backup.timer | 10 +++++ deploy/systemd/mcr-web.service | 31 +++++++++++++++ deploy/systemd/mcr.service | 34 +++++++++++++++++ 10 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 Dockerfile.api create mode 100644 Dockerfile.web create mode 100644 deploy/docker/docker-compose-rift.yml create mode 100644 deploy/mcr-rift.toml create mode 100755 deploy/scripts/install.sh create mode 100644 deploy/systemd/mcr-backup.service create mode 100644 deploy/systemd/mcr-backup.timer create mode 100644 deploy/systemd/mcr-web.service create mode 100644 deploy/systemd/mcr.service diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..5a7279c --- /dev/null +++ b/Dockerfile.api @@ -0,0 +1,37 @@ +FROM golang:1.25-alpine AS builder + +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 /mcrsrv ./cmd/mcrsrv + +FROM alpine:3.21 + +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -S mcr \ + && adduser -S -G mcr -h /srv/mcr -s /sbin/nologin mcr \ + && mkdir -p /srv/mcr && chown mcr:mcr /srv/mcr + +COPY --from=builder /mcrsrv /usr/local/bin/mcrsrv + +# /srv/mcr is the single volume mount point. +# It must contain: +# mcr.toml — configuration file +# certs/ — TLS certificate and key +# layers/ — blob storage +# uploads/ — in-progress uploads (same filesystem as layers/) +# mcr.db — created automatically on first run +VOLUME /srv/mcr +WORKDIR /srv/mcr + +EXPOSE 8443 +EXPOSE 9443 + +USER mcr + +ENTRYPOINT ["mcrsrv"] +CMD ["server", "--config", "/srv/mcr/mcr.toml"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..ec9516c --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,33 @@ +FROM golang:1.25-alpine AS builder + +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 /mcr-web ./cmd/mcr-web + +FROM alpine:3.21 + +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -S mcr \ + && adduser -S -G mcr -h /srv/mcr -s /sbin/nologin mcr \ + && mkdir -p /srv/mcr && chown mcr:mcr /srv/mcr + +COPY --from=builder /mcr-web /usr/local/bin/mcr-web + +# /srv/mcr is the single volume mount point. +# It must contain: +# mcr.toml — configuration file +# certs/ — TLS certificate and key +VOLUME /srv/mcr +WORKDIR /srv/mcr + +EXPOSE 8080 + +USER mcr + +ENTRYPOINT ["mcr-web"] +CMD ["--config", "/srv/mcr/mcr.toml"] diff --git a/PROGRESS.md b/PROGRESS.md index f43cfd1..7f3d21d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -6,8 +6,8 @@ See `PROJECT_PLAN.md` for the implementation roadmap and ## Current State -**Phase:** 12 complete, ready for Phase 13 -**Last updated:** 2026-03-19 +**Phase:** 13 complete +**Last updated:** 2026-03-25 ### Completed @@ -31,7 +31,30 @@ See `PROJECT_PLAN.md` for the implementation roadmap and ### Next Steps -1. Phase 13 (deployment artifacts) +1. Deploy to rift (issue MCR service token, generate TLS cert, update mc-proxy routes) + +### 2026-03-25 — Phase 13: Deployment Artifacts + +**Task:** Create Dockerfiles, systemd units, install script, and rift deployment config. + +**Changes:** + +Step 13.1 — Dockerfiles: +- `Dockerfile.api`: Multi-stage build for mcrsrv (golang:1.25-alpine → alpine:3.21, + non-root `mcr` user, ports 8443/9443, volume /srv/mcr) +- `Dockerfile.web`: Multi-stage build for mcr-web (same pattern, port 8080) + +Step 13.2 — systemd units: +- `deploy/systemd/mcr.service`: Registry server with full security hardening +- `deploy/systemd/mcr-web.service`: Web UI with read-only /srv/mcr +- `deploy/systemd/mcr-backup.service`: Oneshot snapshot + 30-day prune +- `deploy/systemd/mcr-backup.timer`: Daily 02:00 UTC with 5-min jitter + +Step 13.3 — Install script and configs: +- `deploy/scripts/install.sh`: Idempotent install (user, binaries, dirs, units) +- `deploy/mcr-rift.toml`: Rift-specific config (MCIAS auth, TLS, storage paths) +- `deploy/docker/docker-compose-rift.yml`: Docker compose for rift with + loopback port bindings (28443, 29443, 28080) for mc-proxy fronting --- diff --git a/deploy/docker/docker-compose-rift.yml b/deploy/docker/docker-compose-rift.yml new file mode 100644 index 0000000..a7717a3 --- /dev/null +++ b/deploy/docker/docker-compose-rift.yml @@ -0,0 +1,46 @@ +# MCR on rift — container registry. +# +# Two containers: API server (mcrsrv) and web UI (mcr-web). +# Both bind to loopback; mc-proxy handles external TLS ingress. +# +# Usage: +# docker compose -f deploy/docker/docker-compose-rift.yml up -d +# +# Prerequisites: +# - /srv/mcr/mcr.toml (copy from deploy/mcr-rift.toml) +# - /srv/mcr/certs/ with TLS cert+key +# - MCIAS service token for the 'mcr' account + +services: + mcr: + build: + context: ../.. + dockerfile: Dockerfile.api + container_name: mcr + restart: unless-stopped + user: "0:0" + ports: + - "127.0.0.1:28443:8443" + - "127.0.0.1:29443:9443" + volumes: + - /srv/mcr:/srv/mcr + healthcheck: + test: ["CMD", "mcrsrv", "status", "--addr", "https://localhost:8443", "--ca-cert", "/srv/mcr/certs/ca.pem"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + mcr-web: + build: + context: ../.. + dockerfile: Dockerfile.web + container_name: mcr-web + restart: unless-stopped + user: "0:0" + ports: + - "127.0.0.1:28080:8080" + volumes: + - /srv/mcr:/srv/mcr + depends_on: + - mcr diff --git a/deploy/mcr-rift.toml b/deploy/mcr-rift.toml new file mode 100644 index 0000000..c793c49 --- /dev/null +++ b/deploy/mcr-rift.toml @@ -0,0 +1,37 @@ +# MCR configuration for rift. +# +# Container registry fronted by mc-proxy: +# :8443 → mcr API (L4 passthrough via mc-proxy) +# :443 → mcr-web (L7 via mc-proxy) +# +# Copy to /srv/mcr/mcr.toml on rift before starting. + +[server] +listen_addr = ":8443" +grpc_addr = ":9443" +tls_cert = "/srv/mcr/certs/mcr.pem" +tls_key = "/srv/mcr/certs/mcr.key" +read_timeout = "30s" +write_timeout = "0s" +idle_timeout = "120s" +shutdown_timeout = "60s" + +[database] +path = "/srv/mcr/mcr.db" + +[storage] +layers_path = "/srv/mcr/layers" +uploads_path = "/srv/mcr/uploads" + +[mcias] +server_url = "https://mcias.metacircular.net:8443" +ca_cert = "/srv/mcr/certs/ca.pem" +service_token = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL21jaWFzLm1ldGFjaXJjdWxhci5uZXQiLCJzdWIiOiIwYWM3NDk3ZS0wZTE5LTRhOWMtYWI3Yi03YWZjMzc0ZDU3NzIiLCJleHAiOjE4MDYwMzczNzMsIm5iZiI6MTc3NDUwMTM3MywiaWF0IjoxNzc0NTAxMzczLCJqdGkiOiI1NTM0ZDU0OS1kYzY5LTRiNzctYTY5MC0xNzQ3NjE0MDUzYzEiLCJyb2xlcyI6bnVsbH0.bsnoGMrFzJJCIanGuiAvpqmlO2OssvFjYynQgiSt_TPMuLxziRuwuRIL9C_kRnHdF7C6c1mTHncKVj1hkLPiCg" + +[web] +listen_addr = ":8080" +grpc_addr = "mcr:9443" +ca_cert = "/srv/mcr/certs/ca.pem" + +[log] +level = "info" diff --git a/deploy/scripts/install.sh b/deploy/scripts/install.sh new file mode 100755 index 0000000..dd1cfb5 --- /dev/null +++ b/deploy/scripts/install.sh @@ -0,0 +1,55 @@ +#!/bin/sh +set -eu + +SERVICE="mcr" +BINARY_SRV="/usr/local/bin/mcrsrv" +BINARY_WEB="/usr/local/bin/mcr-web" +BINARY_CTL="/usr/local/bin/mcrctl" +DATA_DIR="/srv/${SERVICE}" +UNIT_DIR="/etc/systemd/system" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Create system user and group (idempotent). +if ! id -u "${SERVICE}" >/dev/null 2>&1; then + useradd --system --no-create-home --shell /usr/sbin/nologin "${SERVICE}" + echo "Created system user ${SERVICE}." +fi + +# Install binaries. +install -m 0755 "${REPO_DIR}/mcrsrv" "${BINARY_SRV}" +install -m 0755 "${REPO_DIR}/mcr-web" "${BINARY_WEB}" +install -m 0755 "${REPO_DIR}/mcrctl" "${BINARY_CTL}" +echo "Installed binaries." + +# Create data directory structure. +install -d -o "${SERVICE}" -g "${SERVICE}" -m 0700 "${DATA_DIR}" +install -d -o "${SERVICE}" -g "${SERVICE}" -m 0700 "${DATA_DIR}/backups" +install -d -o "${SERVICE}" -g "${SERVICE}" -m 0700 "${DATA_DIR}/certs" +install -d -o "${SERVICE}" -g "${SERVICE}" -m 0700 "${DATA_DIR}/layers" +install -d -o "${SERVICE}" -g "${SERVICE}" -m 0700 "${DATA_DIR}/uploads" +echo "Created ${DATA_DIR}/." + +# Install example config if none exists. +if [ ! -f "${DATA_DIR}/${SERVICE}.toml" ]; then + install -o "${SERVICE}" -g "${SERVICE}" -m 0600 \ + "${REPO_DIR}/deploy/examples/mcr.toml" \ + "${DATA_DIR}/${SERVICE}.toml" + echo "Installed example config to ${DATA_DIR}/${SERVICE}.toml — edit before starting." +fi + +# Install systemd units. +install -m 0644 "${REPO_DIR}/deploy/systemd/${SERVICE}.service" "${UNIT_DIR}/" +install -m 0644 "${REPO_DIR}/deploy/systemd/${SERVICE}-web.service" "${UNIT_DIR}/" +install -m 0644 "${REPO_DIR}/deploy/systemd/${SERVICE}-backup.service" "${UNIT_DIR}/" +install -m 0644 "${REPO_DIR}/deploy/systemd/${SERVICE}-backup.timer" "${UNIT_DIR}/" +systemctl daemon-reload +echo "Installed systemd units." + +echo "" +echo "Done. Next steps:" +echo " 1. Edit ${DATA_DIR}/${SERVICE}.toml" +echo " 2. Place TLS certs in ${DATA_DIR}/certs/" +echo " 3. systemctl enable --now ${SERVICE}" +echo " 4. systemctl enable --now ${SERVICE}-web" +echo " 5. systemctl enable --now ${SERVICE}-backup.timer" diff --git a/deploy/systemd/mcr-backup.service b/deploy/systemd/mcr-backup.service new file mode 100644 index 0000000..29833ee --- /dev/null +++ b/deploy/systemd/mcr-backup.service @@ -0,0 +1,25 @@ +[Unit] +Description=MCR Database Backup + +[Service] +Type=oneshot +User=mcr +Group=mcr +ExecStart=/usr/local/bin/mcrsrv snapshot --config /srv/mcr/mcr.toml +ExecStartPost=/usr/bin/find /srv/mcr/backups -name 'mcr-*.db' -mtime +30 -delete + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +RestrictNamespaces=true +LockPersonality=true +MemoryDenyWriteExecute=true +RestrictRealtime=true +ReadWritePaths=/srv/mcr diff --git a/deploy/systemd/mcr-backup.timer b/deploy/systemd/mcr-backup.timer new file mode 100644 index 0000000..5dc7ab2 --- /dev/null +++ b/deploy/systemd/mcr-backup.timer @@ -0,0 +1,10 @@ +[Unit] +Description=MCR Daily Database Backup + +[Timer] +OnCalendar=*-*-* 02:00:00 UTC +RandomizedDelaySec=300 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/deploy/systemd/mcr-web.service b/deploy/systemd/mcr-web.service new file mode 100644 index 0000000..0e3e2da --- /dev/null +++ b/deploy/systemd/mcr-web.service @@ -0,0 +1,31 @@ +[Unit] +Description=MCR Container Registry Web UI +After=mcr.service +Wants=mcr.service + +[Service] +Type=simple +User=mcr +Group=mcr +ExecStart=/usr/local/bin/mcr-web --config /srv/mcr/mcr.toml +Restart=on-failure +RestartSec=5 + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +RestrictNamespaces=true +LockPersonality=true +MemoryDenyWriteExecute=true +RestrictRealtime=true +ReadOnlyPaths=/srv/mcr + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/mcr.service b/deploy/systemd/mcr.service new file mode 100644 index 0000000..2eba46a --- /dev/null +++ b/deploy/systemd/mcr.service @@ -0,0 +1,34 @@ +[Unit] +Description=MCR Container Registry +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=mcr +Group=mcr +ExecStart=/usr/local/bin/mcrsrv server --config /srv/mcr/mcr.toml +Restart=on-failure +RestartSec=5 + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +RestrictNamespaces=true +LockPersonality=true +MemoryDenyWriteExecute=true +RestrictRealtime=true +ReadWritePaths=/srv/mcr + +# Allow binding to privileged ports if needed +AmbientCapabilities=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target