From ea3d7de9b28524f066506e44bcf42d80b2077e9a Mon Sep 17 00:00:00 2001 From: tmphost-dev Date: Fri, 23 Oct 2026 04:58:20 +0000 Subject: [PATCH] Build the Docker image from scratch The final stage held distroless's base files the server never uses. It now holds only the binary, the web app, CA certs, a passwd entry and /data. The user keeps UID 65532 so volumes chowned for the old image still work. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 4 ++++ Dockerfile | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d74dde2..4c06b23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,10 @@ app state). A handler never calls the repository directly. - **Streams go on the stdlib mux, not Fiber** (`handler/live`). The Fiber adaptor buffers the whole response, so an SSE route on Fiber sends nothing until it ends. The live query feed (`service/feed`) is in-process only. +- **The image is `FROM scratch`.** It has only what the Dockerfile copies + in: the binary, the web app, CA certs, `/etc/passwd` and `/data`. A new + runtime need (`/tmp`, tzdata) must be copied too, or it fails only in Docker. + It runs as UID 65532 so volumes chowned for the old distroless image work. - **Vite builds with `base: ''`.** The server rewrites `./` to `/` in `index.html` (`app.spa`). Without that rewrite, a hard reload on a nested route such as `/databases/x` serves a blank page. diff --git a/Dockerfile b/Dockerfile index 379ac1f..cdf7166 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,19 +6,27 @@ COPY frontend/ ./ RUN npm run build FROM golang:1.27-alpine AS api +RUN apk add --no-cache ca-certificates WORKDIR /src/backend COPY backend/go.mod backend/go.sum ./ RUN go mod download COPY backend/ ./ -RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /sql-mcp ./cmd/server && mkdir -p /data +RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /sql-mcp ./cmd/server +# scratch has no users: name UID 65532 (distroless's nonroot, kept so volumes +# chowned for the old image still work) and create the volume it must own. +RUN echo 'nonroot:x:65532:65532:nonroot:/:' > /etc_passwd && mkdir -p /data -FROM gcr.io/distroless/static-debian12:nonroot -WORKDIR /app +# scratch holds only the binary, the web app, CA certs (Postgres TLS, tmphost, +# ACME) and /data: no shell or package manager to exploit. +FROM scratch +COPY --from=api /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=api /etc_passwd /etc/passwd COPY --from=api /sql-mcp /app/sql-mcp COPY --from=web /src/frontend/dist /app/public -# The nonroot user must own the volume to create the database and certificates. COPY --from=api --chown=65532:65532 /data /data +WORKDIR /app ENV PUBLIC_DIR=/app/public APP_DB_DSN=/data/sqlmcp.db SSL_CACHE_DIR=/data/certs VOLUME /data EXPOSE 3000 3443 +USER nonroot ENTRYPOINT ["/app/sql-mcp"]