# Multi-stage Dockerfile for ActaLog # Builds both frontend and backend in a single optimized image # ============================================================================ # Stage 1: Build Frontend (Vue.js) # ============================================================================ FROM node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS frontend-builder WORKDIR /app/web # Copy package files COPY web/package.json web/package-lock.json ./ # Pin npm to the version that generated package-lock.json. The node:24-alpine # base image's bundled npm floats across rebuilds; a newer npm reshapes the # rolldown wasm-fallback @emnapi optional deps and breaks `npm ci`. Keep this in # lockstep with the "packageManager" field in web/package.json. RUN npm i -g npm@11.16.0 # Install dependencies (including dev dependencies needed for build) RUN npm ci # Copy frontend source COPY web/ ./ # Build frontend for production RUN npm run build # ============================================================================ # Stage 2: Build Backend (Go) # ============================================================================ FROM golang:1.26-alpine@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c AS backend-builder # Install build dependencies RUN apk add --no-cache git gcc musl-dev sqlite-dev WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ # Download dependencies # Allow Go to auto-download required toolchain version ENV GOTOOLCHAIN=auto RUN go mod download # Copy source code COPY cmd/ ./cmd/ COPY configs/ ./configs/ COPY docs/ ./docs/ COPY internal/ ./internal/ COPY pkg/ ./pkg/ # Build the application # CGO is required for SQLite support # GOTOOLCHAIN=auto allows automatic toolchain download if needed RUN CGO_ENABLED=1 GOOS=linux GOTOOLCHAIN=auto go build -a -installsuffix cgo \ -ldflags '-extldflags "-static"' \ -o /app/bin/actalog \ ./cmd/actalog # ============================================================================ # Stage 3: Runtime Image # ============================================================================ FROM alpine:latest@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 # Install runtime dependencies RUN apk add --no-cache \ ca-certificates \ sqlite \ sqlite-libs \ tzdata # Create non-root user RUN addgroup -g 1000 actalog && \ adduser -D -u 1000 -G actalog actalog WORKDIR /app # Copy backend binary from builder COPY --from=backend-builder /app/bin/actalog /app/actalog # Copy frontend build from builder COPY --from=frontend-builder /app/web/dist /app/web/dist # Copy seed CSV files (movements and WODs) COPY seeds/ /app/seeds/ # Copy seed initialization script and entrypoint COPY docker/scripts/init-seeds.sh /app/scripts/init-seeds.sh COPY docker/scripts/entrypoint.sh /app/scripts/entrypoint.sh # Create directories for uploads, database, and backups RUN mkdir -p /app/uploads /app/data /app/backups /app/scripts && \ chmod +x /app/scripts/init-seeds.sh /app/scripts/entrypoint.sh && \ chown -R actalog:actalog /app # Switch to non-root user USER actalog # Expose port EXPOSE 8080 # Health check # Probe 127.0.0.1 rather than "localhost": where localhost resolves to ::1 first, # wget targets [::1] while the server binds IPv4 only, so the check never passes. # Shell form (not exec form) so ${SERVER_PORT} is expanded at runtime. HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider "http://127.0.0.1:${SERVER_PORT:-8080}/health" || exit 1 # Set default environment variables ENV DB_DRIVER=sqlite3 \ DB_NAME=/app/data/actalog.db \ PORT=8080 \ UPLOADS_DIR=/app/uploads \ FRONTEND_DIR=/app/web/dist # Run the application via entrypoint (handles optional seed import) CMD ["/app/scripts/entrypoint.sh"]