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"]