diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..a55e348 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,93 @@ +# Multi-stage Dockerfile for the Xtablo Go backend. +# +# Three stages: +# assets — downloads Tailwind CLI, HTMX, and Sortable.js then compiles the +# Tailwind output CSS from tailwind.input.css + templates/. (D-09) +# builder — copies assets, runs `templ generate`, and compiles both binaries +# with CGO_ENABLED=0 so they are fully static. (D-07) +# runtime — copies only the two binaries into a minimal distroless image. +# No CMD is set — docker-compose overrides command: per service. (D-08) +# +# Build context: backend/ directory (sibling to go.mod). +# From the repo root: docker build -f backend/Dockerfile backend/ +# From inside backend/: docker build . +# +# Security: no .env files are ever COPY'd into any layer (T-07-05). +# The :nonroot distroless tag runs as uid 65532, preventing filesystem writes (T-07-07). + +# --------------------------------------------------------------------------- +# Stage 1: assets +# Downloads pinned versions of Tailwind CLI, HTMX, and Sortable.js, +# then compiles tailwind.input.css against templates/ for the minified CSS. +# --------------------------------------------------------------------------- +FROM node:20-alpine AS assets + +WORKDIR /build + +RUN apk add --no-cache curl + +RUN mkdir -p static + +# Tailwind standalone CLI — pinned at v4.0.0 (matches justfile tailwind_version) +RUN curl -sSL -o /usr/local/bin/tailwindcss \ + "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.0.0/tailwindcss-linux-x64" \ + && chmod +x /usr/local/bin/tailwindcss + +# HTMX — pinned at major version 2 (matches justfile htmx_version) +RUN curl -sSL -o static/htmx.min.js \ + "https://unpkg.com/htmx.org@2/dist/htmx.min.js" + +# Sortable.js — pinned at v1.15.7 (matches justfile sortable_version) +RUN curl -sSL -o static/sortable.min.js \ + "https://cdn.jsdelivr.net/npm/sortablejs@1.15.7/Sortable.min.js" + +# Copy Tailwind input and templates for content-scanning +COPY tailwind.input.css . +COPY templates/ templates/ + +# Compile and minify Tailwind CSS +RUN tailwindcss -i tailwind.input.css -o static/tailwind.css --minify + +# --------------------------------------------------------------------------- +# Stage 2: builder +# Compiles both Go binaries (cmd/web and cmd/worker) with CGO_ENABLED=0. +# Runs `templ generate` first since *_templ.go files are gitignored. (D-07) +# --------------------------------------------------------------------------- +FROM golang:1.26-alpine AS builder + +WORKDIR /app + +# Download dependencies first (layer-cached until go.mod/go.sum change) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the entire backend context +COPY . . + +# Overwrite static/ with the freshly built assets from the assets stage +COPY --from=assets /build/static ./static + +# Install templ at the pinned version and run template generation +# (gitignored *_templ.go files must be generated at build time) +RUN go install github.com/a-h/templ/cmd/templ@v0.3.1020 && templ generate + +# Compile the web server binary — CGO_ENABLED=0 required for distroless/static +RUN --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o /app/web ./cmd/web + +# Compile the background worker binary — CGO_ENABLED=0 required for distroless/static +RUN --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o /app/worker ./cmd/worker + +# --------------------------------------------------------------------------- +# Stage 3: runtime +# Minimal distroless image containing only the two compiled binaries. +# Runs as nonroot (uid 65532) — no filesystem write access. (T-07-07) +# No CMD set — docker-compose.prod.yaml provides `command:` per service. (D-08) +# --------------------------------------------------------------------------- +FROM gcr.io/distroless/static-debian12:nonroot + +COPY --from=builder /app/web /app/web +COPY --from=builder /app/worker /app/worker + +EXPOSE 8080