From 4dfc7baa2d818a26825dbf0019795fdb707aaf43 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Fri, 2 Oct 2026 22:50:16 -0400 Subject: [PATCH 1/3] Add Makefile dev/test/docker targets and multi-stage Dockerfile Makefile: dev runs the Go server (:8080) and Vite (:5173) together and stops both on Ctrl-C; test runs the gofmt check, go vet, go test -race and npm ci/typecheck/test; build stamps the version via -ldflags into a static CGO_ENABLED=0 binary; docker builds the root Dockerfile. VERSION defaults to git describe with a dev fallback. Dockerfile: digest-pinned Node 22, Go 1.25 and Alpine 3.22 stages; the runtime runs as uid 10001 with /data as a volume, EXPOSE 8080 and a HEALTHCHECK on /health. .dockerignore keeps VCS, design, dependencies, build output, databases and secrets out of the context. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 20 ++++++++++++++++++++ Dockerfile | 37 +++++++++++++++++++++++++++++++++++++ Makefile | 39 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..420c29b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +# Keep the build context small and free of secrets and local state. +.git +.gitignore +.dockerignore +Dockerfile +.remember/ +.claude/ +design/ +**/.DS_Store +node_modules +web/node_modules +web/dist +bin/ +*.db +*.db-* +.env* +**/.env* +deploy/secrets/ +*.pem +*.key diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8faa3d1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1 +# Base images are pinned by digest (multi-arch index); tags are for readability. + +FROM node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 AS web +WORKDIR /src/web +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web/ ./ +RUN npm run build + +FROM golang:1.25-alpine@sha256:1ae0735f00daffa3aaf1363a5184c0d2dc55c78e3db4ec70241cdac97bf84b59 AS server +ARG VERSION=dev +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +# Replace the placeholder with the built PWA; recreate the directory so no +# stale file survives into the embed. +RUN rm -rf internal/frontend/dist +COPY --from=web /src/web/dist/ internal/frontend/dist/ +RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" \ + -o /out/mealquest ./cmd/server + +# Alpine ships the CA bundle and busybox wget for the health check. +FROM alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 +RUN addgroup -S -g 10001 mealquest \ + && adduser -S -D -H -u 10001 -G mealquest mealquest \ + && mkdir /data && chown mealquest:mealquest /data +COPY --from=server /out/mealquest /usr/local/bin/mealquest +ENV MEALQUEST_HTTP_ADDR=:8080 \ + MEALQUEST_DATA_DIR=/data +VOLUME /data +EXPOSE 8080 +USER 10001:10001 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1 +ENTRYPOINT ["/usr/local/bin/mealquest"] diff --git a/Makefile b/Makefile index 3917323..bbdf37e 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,41 @@ -# Build entry point: build the PWA, embed it, then build the server binary. +# MealQuest build entry points. +# +# make dev Go server on :8080 plus Vite dev server on :5173 (proxies +# /api, /auth, /media to :8080). Needs the MEALQUEST_* env set. +# make test gofmt check, go vet, go test -race, npm typecheck and test. +# make build Build the PWA, embed it, build the static binary bin/mealquest. +# make docker Build the container image from ./Dockerfile. FRONTEND_DIST := internal/frontend/dist +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +IMAGE ?= mealquest:$(VERSION) +LDFLAGS := -s -w -X main.version=$(VERSION) -.PHONY: build web embed check-embed-clean +.PHONY: dev test go-test web-test build web embed check-embed-clean docker + +# Run both servers; Ctrl-C (or either exiting) stops both. The binary is built +# first and run directly because `go run` would orphan its child on kill. +dev: SHELL := /bin/bash +dev: + go build -ldflags "$(LDFLAGS)" -o bin/mealquest-dev ./cmd/server + @trap 'kill $$(jobs -p) 2>/dev/null; wait' EXIT; \ + bin/mealquest-dev & \ + (cd web && exec npm run dev) & \ + wait -n + +test: go-test web-test + +go-test: + @unformatted="$$(gofmt -l .)"; \ + if [ -n "$$unformatted" ]; then echo "gofmt needed:"; echo "$$unformatted"; exit 1; fi + go vet ./... + go test -race ./... + +web-test: + cd web && npm ci && npm run typecheck && npm test build: embed - go build -o bin/mealquest ./cmd/server + CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/mealquest ./cmd/server web: cd web && npm ci && npm run build @@ -22,3 +52,6 @@ check-embed-clean: touch $(FRONTEND_DIST)/.stale-embed-check $(MAKE) embed test ! -e $(FRONTEND_DIST)/.stale-embed-check + +docker: + docker build --build-arg VERSION=$(VERSION) -t $(IMAGE) . -- 2.55.0 From 2e5724d15009ee8d41e101d79c4aa6ea7494ba04 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Fri, 2 Oct 2026 22:55:44 -0400 Subject: [PATCH 2/3] Fix review findings F1 and F2 for the Dockerfile and .dockerignore F1: derive the HEALTHCHECK port from MEALQUEST_HTTP_ADDR so an overridden listen port is still probed at /health. F2: make key/certificate exclusions recursive (**/*.pem, **/*.key) and add **/*.p12, **/*.pfx, **/*.jks, **/*.keystore so nested local secrets never enter the build context. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 8 ++++++-- Dockerfile | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 420c29b..3cbf0a8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,5 +16,9 @@ bin/ .env* **/.env* deploy/secrets/ -*.pem -*.key +**/*.pem +**/*.key +**/*.p12 +**/*.pfx +**/*.jks +**/*.keystore diff --git a/Dockerfile b/Dockerfile index 8faa3d1..869a9d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,8 @@ ENV MEALQUEST_HTTP_ADDR=:8080 \ VOLUME /data EXPOSE 8080 USER 10001:10001 +# Probe the port from MEALQUEST_HTTP_ADDR (text after the last colon) so an +# overridden listen address stays healthy. HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1 + CMD wget -q -O /dev/null "http://127.0.0.1:${MEALQUEST_HTTP_ADDR##*:}/health" || exit 1 ENTRYPOINT ["/usr/local/bin/mealquest"] -- 2.55.0 From 9b66ba0ff5ee569aac0077fec56f5498d17cd4be Mon Sep 17 00:00:00 2001 From: dev_agent Date: Fri, 2 Oct 2026 23:02:31 -0400 Subject: [PATCH 3/3] Fix review findings F1-F3 for Docker context, health check and version F1: exclude generated internal/frontend/dist from the Docker build context. F2: derive the health check host from MEALQUEST_HTTP_ADDR, mapping wildcard hosts and localhost to 127.0.0.1 and keeping IPv6 brackets. F3: compute the -dirty suffix ignoring the generated embed output, so a rebuild of a clean checkout is not stamped dirty. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 2 ++ Dockerfile | 9 ++++++--- Makefile | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3cbf0a8..9d9544a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,8 @@ design/ node_modules web/node_modules web/dist +# Generated embed output; the Go stage takes the PWA from the web stage. +internal/frontend/dist/ bin/ *.db *.db-* diff --git a/Dockerfile b/Dockerfile index 869a9d7..9ef5a49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,11 @@ ENV MEALQUEST_HTTP_ADDR=:8080 \ VOLUME /data EXPOSE 8080 USER 10001:10001 -# Probe the port from MEALQUEST_HTTP_ADDR (text after the last colon) so an -# overridden listen address stays healthy. +# Probe the listener from MEALQUEST_HTTP_ADDR: host is everything before the +# last colon (IPv6 keeps its brackets). Wildcard hosts map to loopback, as +# does localhost: Go listens on its IPv4 address but wget would try ::1. HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q -O /dev/null "http://127.0.0.1:${MEALQUEST_HTTP_ADDR##*:}/health" || exit 1 + CMD host="${MEALQUEST_HTTP_ADDR%:*}"; \ + case "$host" in ""|0.0.0.0|"[::]"|localhost) host=127.0.0.1 ;; esac; \ + wget -q -O /dev/null "http://${host}:${MEALQUEST_HTTP_ADDR##*:}/health" || exit 1 ENTRYPOINT ["/usr/local/bin/mealquest"] diff --git a/Makefile b/Makefile index bbdf37e..e9f157a 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,10 @@ # make docker Build the container image from ./Dockerfile. FRONTEND_DIST := internal/frontend/dist -VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +# Dirty means a tracked change outside the generated embed output, which +# `make embed` rewrites; plain `--dirty` would flag every rebuild. +GIT_DIRTY := $(shell git status --porcelain --untracked-files=no -- . ':(exclude)$(FRONTEND_DIST)' 2>/dev/null) +VERSION ?= $(shell git describe --tags --always 2>/dev/null || echo dev)$(if $(GIT_DIRTY),-dirty) IMAGE ?= mealquest:$(VERSION) LDFLAGS := -s -w -X main.version=$(VERSION) -- 2.55.0