# Container images Two minimal, non root, FIPS validated images, each a two stage build with the build context at the repository root. - `Dockerfile.mm-api` builds `cmd/api` (the API server, port 8080). - `Dockerfile.megamind-gateway` builds `cmd/megamind-gateway` (the stateless inference proxy, port 8090). ## Build ``` docker build -f infra/docker/Dockerfile.mm-api -t mm-api:local . docker build -f infra/docker/Dockerfile.megamind-gateway -t megamind-gateway:local . ``` Each build cross compiles the binary with the validated FIPS module (`CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFIPS140=v1.0.0`, identical to the Makefile `build` target) and FAILS the build unless the artifact carries the stamp (`go version -m /out/ | grep -q GOFIPS140=v1.0.0`, the same assertion as `make verify-fips`). So a non FIPS binary can never be packaged. ## Verify the FIPS stamp inside the built image The in build gate already guarantees it, but to confirm on the shipped image: ``` docker run --rm --entrypoint /busybox/sh mm-api:local -c 'true' # distroless has no shell; instead: # extract and check on the host: id=$(docker create mm-api:local) && docker cp "$id":/mm-api /tmp/mm-api && docker rm "$id" go version -m /tmp/mm-api | grep GOFIPS140 # expect: GOFIPS140=v1.0.0 ``` The running container also defaults `GODEBUG=fips140=on` (stamped in by `GOFIPS140=v1.0.0`) and runs the in process FIPS guard plus the PBKDF2 self test at boot, so a non FIPS runtime fails closed before serving. ## Base image digests (pinned) Both Dockerfiles pin the builder and the runtime base by sha256 digest. Re resolve them (and let Renovate or Dependabot, which both understand `FROM ...@sha256`, open re pin pull requests): ``` # golang:1.26.4 (multi arch index) tok=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/golang:pull" | jq -r .token) curl -sI -H "Authorization: Bearer $tok" -H "Accept: application/vnd.oci.image.index.v1+json" https://registry-1.docker.io/v2/library/golang/manifests/1.26.4 | grep -i docker-content-digest # gcr.io/distroless/static-debian12:nonroot tok=$(curl -s "https://gcr.io/v2/token?scope=repository:distroless/static-debian12:pull" | jq -r .token) curl -sI -H "Authorization: Bearer $tok" -H "Accept: application/vnd.oci.image.index.v1+json" https://gcr.io/v2/distroless/static-debian12/manifests/nonroot | grep -i docker-content-digest ``` After any base bump, re run `make verify-fips` and rebuild the image so the in build stamp gate runs again. ## Runtime hardening (orchestration layer) The Dockerfile sets the non root numeric user; the read only root filesystem, the writable `/tmp` tmpfs, and dropping all Linux capabilities are enforced by the pod spec (a container image cannot set them itself), per the NIST 800-171 baseline. Run both images with: ```yaml securityContext: # pod runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 fsGroup: 65532 seccompProfile: { type: RuntimeDefault } containers: - name: mm-api securityContext: # container readOnlyRootFilesystem: true allowPrivilegeEscalation: false privileged: false capabilities: { drop: [ALL] } volumeMounts: - { name: tmp, mountPath: /tmp } volumes: - name: tmp emptyDir: { medium: Memory } ``` Neither entrypoint writes to disk outside `/tmp` (no `os.Create`, `WriteFile`, or `MkdirAll` in the request path), so `/tmp` is the only writable mount required. ## Notes - distroless static, NOT scratch: the app makes verify full TLS connections (managed Postgres, the cloud secret store, the object store), so the `ca-certificates` bundle in static is load bearing. - Never set `GODEBUG=fips140=only` in a production image or pod; the Go docs call it best effort and crash by design. It belongs only in a dedicated CI probing stage. - The gateway image proxies Ollama over `MEGAMIND_OLLAMA_URL` (set at deploy); it carries no model and no Python.