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) <noreply@anthropic.com>
This commit is contained in:
37
Dockerfile.api
Normal file
37
Dockerfile.api
Normal file
@@ -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"]
|
||||||
33
Dockerfile.web
Normal file
33
Dockerfile.web
Normal file
@@ -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"]
|
||||||
29
PROGRESS.md
29
PROGRESS.md
@@ -6,8 +6,8 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
|
|||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
**Phase:** 12 complete, ready for Phase 13
|
**Phase:** 13 complete
|
||||||
**Last updated:** 2026-03-19
|
**Last updated:** 2026-03-25
|
||||||
|
|
||||||
### Completed
|
### Completed
|
||||||
|
|
||||||
@@ -31,7 +31,30 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
|
|||||||
|
|
||||||
### Next Steps
|
### 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
46
deploy/docker/docker-compose-rift.yml
Normal file
46
deploy/docker/docker-compose-rift.yml
Normal file
@@ -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
|
||||||
37
deploy/mcr-rift.toml
Normal file
37
deploy/mcr-rift.toml
Normal file
@@ -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"
|
||||||
55
deploy/scripts/install.sh
Executable file
55
deploy/scripts/install.sh
Executable file
@@ -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"
|
||||||
25
deploy/systemd/mcr-backup.service
Normal file
25
deploy/systemd/mcr-backup.service
Normal file
@@ -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
|
||||||
10
deploy/systemd/mcr-backup.timer
Normal file
10
deploy/systemd/mcr-backup.timer
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MCR Daily Database Backup
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 02:00:00 UTC
|
||||||
|
RandomizedDelaySec=300
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
31
deploy/systemd/mcr-web.service
Normal file
31
deploy/systemd/mcr-web.service
Normal file
@@ -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
|
||||||
34
deploy/systemd/mcr.service
Normal file
34
deploy/systemd/mcr.service
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user