# Multi-stage build: build web bundle with Node, then build Go server, then produce a small runtime image # 1) Build Web Components bundle (esbuild) FROM node:20-alpine AS webbuild WORKDIR /app # Copy only what we need for the web build first (faster caching) COPY package.json package-lock.json ./ COPY web ./web # Ensure output directory exists (esbuild writes to server/static/js/tap.js) RUN mkdir -p server/static/js RUN npm ci && npm run build:wc # 2) Build Go server FROM golang:1.24-alpine AS gobuild WORKDIR /app/server # Copy Go module and source COPY server/go.mod ./go.mod COPY server/go.sum ./go.sum # Pre-fetch modules for better caching RUN go mod download # Now copy the rest of the source (entire server tree) COPY server/ ./ # Bring in built web assets from the previous stage (overwrites js bundle) COPY --from=webbuild /app/server/static/js /app/server/static/js ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 RUN go build -o /app/server/app . # 3) Runtime image FROM alpine:3.19 AS runtime RUN apk add --no-cache ca-certificates WORKDIR /app/server # Copy binary and runtime assets COPY --from=gobuild /app/server/app ./app COPY --from=gobuild /app/server/templates ./templates COPY --from=gobuild /app/server/static ./static # Fly will set PORT; our server defaults to 8088 if not set ENV PORT=8088 EXPOSE 8088 # Security best-practice: run as non-root RUN addgroup -S app && adduser -S app -G app USER app CMD ["./app"]