forgejo-mcp-broker/Containerfile
Ole-Morten Duesund 018f56a4ad feat(deploy): rootless podman + Quadlet deployment (forgejo-mcp-broker-8yd)
Adds a multi-stage Containerfile, Quadlet unit, and operator
walkthrough for a production deploy. The broker spawns forgejo-mcp
per session, so the image bundles both binaries — broker built from
this repo, forgejo-mcp pinned via FORGEJO_MCP_VERSION build-arg
(default 2.18.0).

Image stages:
  1. golang:alpine compiles the broker with ldflags-stamped buildinfo
  2. golang:alpine clones forgejo-mcp at the pinned tag and compiles it
  3. distroless static-nonroot copies both binaries; uid 65532

Persistent state via the named volume `fjmcp-state` mounted at /data.
SQLite WAL + SHM sidecars live alongside broker.db on the same volume,
so a container swap or image upgrade preserves all OAuth clients,
issued tokens, and refresh-token history. Verified end-to-end:

  podman run --rm -d -v fjmcp-test-state:/data ... fjmcp-broker:test
  curl /healthz                              # store: ok, broker.db created
  podman stop fjmcp-test
  podman run --rm -d -v fjmcp-test-state:/data ... fjmcp-broker:test
  curl /healthz                              # store: ok, same broker.db

  ls volume       → broker.db, broker.db-shm, broker.db-wal all present

Quadlet unit (deploy/podman/fjmcp-broker.container) drops into
~/.config/containers/systemd/, reads secrets from a 0600 env file
outside the unit, publishes :8080 on loopback for Caddy to front.

Makefile gains `image` and `image-run` targets. README links to the
new docs/deploy-podman.md walkthrough.

Closes forgejo-mcp-broker-8yd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:42:09 +02:00

73 lines
2.8 KiB
Docker

# Multi-stage Containerfile for fjmcp-broker.
#
# Stage 1: build fjmcp-broker (this repo).
# Stage 2: build forgejo-mcp at a pinned version. The broker spawns it as
# a subprocess per session, so it must live in the final image.
# Stage 3: distroless static-nonroot. CGO_ENABLED=0 in both build stages
# keeps the final image free of glibc/musl entanglements and
# makes the image entirely static.
#
# Build labels and /etc/build-info wire BUILD_DATE and GIT_REVISION so
# operators can correlate a running container back to a commit.
ARG BUILD_DATE="unknown"
ARG GIT_REVISION="unknown"
ARG FORGEJO_MCP_VERSION="2.18.0"
# ---------- Stage 1: broker build ----------
FROM docker.io/library/golang:1.26-alpine AS broker-build
WORKDIR /src
RUN apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG BUILD_DATE
ARG GIT_REVISION
ENV CGO_ENABLED=0
RUN go build \
-trimpath \
-ldflags "-s -w \
-X kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo.Version=${GIT_REVISION} \
-X kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo.GitRevision=${GIT_REVISION} \
-X kode.naiv.no/olemd/forgejo-mcp-broker/internal/buildinfo.BuildDate=${BUILD_DATE}" \
-o /out/fjmcp-broker \
./cmd/broker
# ---------- Stage 2: forgejo-mcp build ----------
FROM docker.io/library/golang:1.26-alpine AS mcp-build
WORKDIR /src
RUN apk add --no-cache git
ARG FORGEJO_MCP_VERSION
RUN git clone --depth=1 --branch v${FORGEJO_MCP_VERSION} \
https://codeberg.org/goern/forgejo-mcp.git . \
&& CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/forgejo-mcp .
# ---------- Stage 3: runtime ----------
FROM gcr.io/distroless/static-debian12:nonroot
ARG BUILD_DATE
ARG GIT_REVISION
ARG FORGEJO_MCP_VERSION
LABEL org.opencontainers.image.title="fjmcp-broker"
LABEL org.opencontainers.image.description="OAuth 2.1 broker that fronts forgejo-mcp for MCP clients."
LABEL org.opencontainers.image.source="https://kode.naiv.no/olemd/forgejo-mcp-broker"
LABEL org.opencontainers.image.created="${BUILD_DATE}"
LABEL org.opencontainers.image.revision="${GIT_REVISION}"
LABEL org.opencontainers.image.licenses="MIT"
LABEL net.naiv.fjmcp.forgejo-mcp-version="${FORGEJO_MCP_VERSION}"
COPY --from=broker-build /out/fjmcp-broker /usr/local/bin/fjmcp-broker
COPY --from=mcp-build /out/forgejo-mcp /usr/local/bin/forgejo-mcp
# Persistent volume for the SQLite store (clients, tokens, audit data).
# Operators mount a named podman volume here so state survives container
# replacement. SQLite WAL writes auxiliary files (.db-wal, .db-shm) next
# to the main file — the volume contains them all.
VOLUME ["/data"]
WORKDIR /data
# Distroless nonroot user is uid 65532. Volumes inherit ownership from the
# host; document the chown step in deploy-podman.md.
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/fjmcp-broker"]