# 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