# OrchestrAD Docker Image # Three-stage build: (1) compile the Next.js static export, (2) embed it into # the Go binary, (3) ship a minimal Alpine runtime. The single resulting binary # serves the full product UI plus the API on one port. # --------------------------------------------------------------------------- # Stage 1: build the frontend (Next.js static export -> frontend/out) # Debian base avoids musl/sharp native-module friction; this stage is discarded. # --------------------------------------------------------------------------- FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS frontend WORKDIR /frontend # Install deps first for better layer caching. .npmrc carries # legacy-peer-deps=true, which npm ci needs to resolve the React 19 RC peer # graph, so it must be present before the install runs. COPY frontend/package.json frontend/package-lock.json frontend/.npmrc ./ RUN npm ci --no-audit --no-fund COPY frontend/ ./ RUN npm run build # next.config.mjs sets output:"export", so the static site lands in ./out # --------------------------------------------------------------------------- # Stage 2: build the Go binary with the UI embedded # --------------------------------------------------------------------------- # Pinned to the *build* platform and cross-compiled with GOARCH below, so an # arm64 image is produced natively on an amd64 runner with no QEMU emulation # (which would make the Go build minutes-long instead of seconds). FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder # Pure-Go SQLite (modernc.org/sqlite) means no C toolchain is needed, which is # also what makes cross-compilation this simple. RUN apk add --no-cache git WORKDIR /build # Cache modules first. COPY backend/go.mod backend/go.sum* ./ RUN go mod download # Copy source, then drop the staged UI into the embed directory. webui.go does # //go:embed all:dist, so the compiled binary carries the real UI instead of # the placeholder page. COPY backend/ ./ COPY --from=frontend /frontend/out/ ./internal/webui/dist/ # Build with version info injected via ldflags. ARG VERSION=dev ARG BUILD_TIME=unknown ARG GIT_COMMIT=unknown # TARGETOS/TARGETARCH are supplied automatically by buildx for each --platform. ARG TARGETOS ARG TARGETARCH RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build \ -ldflags "-s -w \ -X github.com/Grace-Solutions/OrchestrAD/internal/version.Version=${VERSION} \ -X github.com/Grace-Solutions/OrchestrAD/internal/version.BuildTime=${BUILD_TIME} \ -X github.com/Grace-Solutions/OrchestrAD/internal/version.GitCommit=${GIT_COMMIT}" \ -o /orchestrad ./cmd/orchestrad # --------------------------------------------------------------------------- # Stage 3: minimal runtime # --------------------------------------------------------------------------- FROM alpine:3.19 RUN apk add --no-cache ca-certificates tzdata wget # Create non-root user RUN addgroup -g 1000 orchestrad && \ adduser -u 1000 -G orchestrad -D orchestrad WORKDIR /app # Copy binary COPY --from=builder /orchestrad /app/orchestrad # Create data directories RUN mkdir -p /data/logs /data/backups && \ chown -R orchestrad:orchestrad /data # Default environment ENV ORCHESTRAD_DATA_PATH=/data ENV ORCHESTRAD_LOG_LEVEL=info # Containers typically sit behind an ingress/proxy that terminates TLS, so the # image serves plain HTTP by default and the healthcheck below is HTTP. Set # ORCHESTRAD_TLS_ENABLED=true to have the container manage its own certificate. ENV ORCHESTRAD_TLS_ENABLED=false # Expose port EXPOSE 18090 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:18090/health || exit 1 # Switch to non-root user USER orchestrad # Default to foreground mode ENTRYPOINT ["/app/orchestrad"] CMD ["run"]