From 33df4d33eb6a8c03fb96d876d41ded547b45370b Mon Sep 17 00:00:00 2001 From: Justin Visser Date: Thu, 11 Jun 2026 14:22:21 +0200 Subject: [PATCH] deploy-only Dockerfile, slice 2 of M4 Three stages: npm build, Go build with the real dist copied over the placeholder before compiling, distroless/static (nonroot) runtime. The container listens on :8080 (all interfaces, unlike the localhost dev default). Verified locally: 16.6 MB image, /, /healthz and /api all answer. Dev loop unchanged, Docker stays out of it. --- .dockerignore | 12 ++++++++++++ Dockerfile | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f50caac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# Keep the build context small and the image layers cache-friendly: +# nothing here is needed to build the frontend or the binary. +.git +.github +docs +node_modules +web/node_modules +web/dist +web/coverage +dev.sh +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6a53d7d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Multi-stage build: the frontend and the Go binary are built in their +# own throwaway stages, and only the final static binary ships. Docker +# is deploy-only; the dev loop stays native (./dev.sh). + +# Stage 1: build the frontend. lts/* in CI; pin the major here so image +# builds don't shift under us when a new LTS lands. +FROM node:22-alpine AS web +WORKDIR /src/web +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web/ ./ +RUN npm run build + +# Stage 2: build the binary, with the real frontend embedded in place of +# the committed placeholder (see internal/webdist). +FROM golang:1.26-alpine AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +COPY --from=web /src/web/dist/ internal/webdist/dist/ +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /spreadlab ./cmd/spreadlab + +# Stage 3: minimal runtime. distroless/static has CA certs and tzdata +# but no shell, which is all a static Go binary needs. +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=build /spreadlab /spreadlab +EXPOSE 8080 +# 0.0.0.0, not the dev default localhost: inside a container the +# loopback interface is unreachable from outside. +ENTRYPOINT ["/spreadlab", "-addr", ":8080"]