Build image / build-and-push (push) Successful in 1m3s
Workflow:
- Pass untrusted github.ref_name/repository/sha/token via env instead of
inline ${{ }} in run: blocks, closing a branch-name shell-injection vector.
- Check out the exact triggering commit (fetch by SHA, branch fallback) so
the sha-<short> tag matches what was built.
- Guard the tag scheme: a non-main branch named/sanitized to "latest" can't
clobber :latest, and an empty sanitized name falls back to branch-<sha>.
- Build --tag flags as a bash array (no word-splitting), drop the
comma-string round-trip, and check-then-create the buildx builder so a
real failure isn't swallowed.
Dockerfile:
- GOWORK=off in the build stage (matches the Makefile convention).
- npm ci uses a BuildKit cache mount.
- Drop the stale issue-number reference in a comment.
.dockerignore:
- Add .env/.env.*, go.work/go.work.sum, web/.vite; drop nonexistent .github.
Graded all findings in the gadfly store. Deferred/keep-as-is: token-in-clone-URL
(Gitea masks the secret in logs), sanitization collisions across branch names
(the sha-<short> tag is the collision-proof identifier), and registry build
cache (current builds are fast). False positives: alpine ships wget (busybox);
docker login is its own step.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
63 lines
2.4 KiB
Docker
63 lines
2.4 KiB
Docker
# syntax=docker/dockerfile:1
|
|
#
|
|
# pansy ships as one static binary with the web build embedded. This is a
|
|
# three-stage build: compile the frontend, embed it into the Go binary, then
|
|
# copy that single binary into a minimal runtime image.
|
|
|
|
# ---- stage 1: build the web bundle ------------------------------------------
|
|
FROM node:22-alpine AS web
|
|
WORKDIR /web
|
|
COPY web/package.json web/package-lock.json ./
|
|
RUN --mount=type=cache,target=/root/.npm npm ci
|
|
COPY web/ ./
|
|
RUN npm run build
|
|
|
|
# ---- stage 2: build the static Go binary (web bundle embedded) --------------
|
|
FROM golang:1.26-alpine AS build
|
|
# GOWORK=off matches the Makefile: pansy is a standalone module, built without
|
|
# any parent workspace even if one leaks into the build context.
|
|
ENV CGO_ENABLED=0 GOTOOLCHAIN=auto GOWORK=off
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
|
COPY . .
|
|
# Overlay the freshly built frontend into the embed directory before compiling
|
|
# (//go:embed all:dist in internal/webdist).
|
|
COPY --from=web /web/dist ./internal/webdist/dist
|
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
|
--mount=type=cache,target=/root/.cache/go-build \
|
|
go build -trimpath -ldflags "-s -w" -o /out/pansy ./cmd/pansy
|
|
|
|
# ---- stage 3: minimal runtime -----------------------------------------------
|
|
FROM alpine:3.20
|
|
ARG BUILD_COMMIT=unknown
|
|
ARG BUILD_BRANCH=unknown
|
|
ARG BUILD_TIME=unknown
|
|
LABEL org.opencontainers.image.title="pansy" \
|
|
org.opencontainers.image.description="Self-hostable garden planner" \
|
|
org.opencontainers.image.source="https://gitea.stevedudenhoeffer.com/steve/pansy" \
|
|
org.opencontainers.image.revision="${BUILD_COMMIT}" \
|
|
org.opencontainers.image.version="${BUILD_BRANCH}" \
|
|
org.opencontainers.image.created="${BUILD_TIME}"
|
|
|
|
# ca-certificates: outbound TLS (e.g. OIDC discovery). tzdata: correct local time.
|
|
RUN apk add --no-cache ca-certificates tzdata \
|
|
&& adduser -D -u 10001 pansy \
|
|
&& mkdir -p /data \
|
|
&& chown pansy:pansy /data
|
|
|
|
COPY --from=build /out/pansy /usr/local/bin/pansy
|
|
|
|
USER pansy
|
|
# The SQLite database lives on the /data volume so it survives container updates.
|
|
ENV PANSY_PORT=8080 \
|
|
PANSY_DB=/data/pansy.db
|
|
WORKDIR /data
|
|
VOLUME /data
|
|
EXPOSE 8080
|
|
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
CMD wget -q -O - "http://127.0.0.1:${PANSY_PORT}/api/v1/healthz" || exit 1
|
|
|
|
ENTRYPOINT ["/usr/local/bin/pansy"]
|