# Prizma — a single static binary with the admin SPA baked in.
#
# Three stages: build the React panel, embed it into the Go binary, ship the
# binary on a bare alpine. Nothing from the build stages survives into the final
# image, so there is no node, no Go toolchain and no source in what you run.

# ---------------------------------------------------------------------------
# 1. The admin panel (web/ -> web/dist)
# ---------------------------------------------------------------------------
FROM node:24-alpine AS web

WORKDIR /web

# Manifests first: this layer is what Docker caches, and it only invalidates
# when the dependency set actually changes — editing a .tsx does not refetch npm.
COPY web/package.json web/package-lock.json* ./

# `npm ci` is the reproducible install and the one CI uses. The fallback exists
# only so a checkout without a committed lockfile still builds an image instead
# of failing at layer 2 with an opaque npm error.
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi

COPY web/ ./
RUN npm run build

# ---------------------------------------------------------------------------
# 2. The Go binary, with web/dist embedded as internal/webui/dist
# ---------------------------------------------------------------------------
FROM golang:1.26-alpine AS build

# Stamped into main.version. CI passes the tag; a manual `docker build` gets "dev".
ARG VERSION=dev
ARG TARGETOS=linux
ARG TARGETARCH

WORKDIR /src

# Module graph first, for the same caching reason as npm above.
COPY go.mod go.sum ./
RUN go mod download

COPY . .

# The repo ships a placeholder internal/webui/dist/index.html so `go build` is
# green before anyone has run npm. Drop it wholesale rather than copying over
# it — a merge would leave stale assets from the placeholder in the image.
RUN rm -rf internal/webui/dist
COPY --from=web /web/dist ./internal/webui/dist

# CGO_ENABLED=0 is not an optimization, it is a requirement: the whole point of
# picking modernc.org/sqlite is that the result is a static, libc-free binary
# that runs on this alpine and on a distroless/scratch image alike.
# -buildvcs=false because .dockerignore keeps .git out of the context.
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
    go build -trimpath -buildvcs=false \
      -ldflags "-s -w -X main.version=${VERSION}" \
      -o /out/prizma ./cmd/prizma

# ---------------------------------------------------------------------------
# 3. Runtime
# ---------------------------------------------------------------------------
FROM alpine:3.21

# ca-certificates: upstream panels are HTTPS, and a scratch trust store means
# every fetch fails with x509. tzdata: cache TTLs and the request log are
# timestamped, and operators expect their own TZ.
RUN apk add --no-cache ca-certificates tzdata

# Fixed uid/gid so a bind-mounted ./data can be chowned predictably on the host.
RUN addgroup -g 10001 -S prizma \
 && adduser  -u 10001 -S -G prizma -h /app -s /sbin/nologin prizma

WORKDIR /app
COPY --from=build /out/prizma /usr/local/bin/prizma

# The DB lives here. Created and chowned BEFORE the VOLUME line so the volume is
# seeded with the right ownership when Docker creates it on first run.
RUN mkdir -p /data && chown -R prizma:prizma /data /app
VOLUME ["/data"]

USER prizma

ENV PRIZMA_ADDR=":8080" \
    PRIZMA_DB="/data/prizma.db" \
    LOG_LEVEL="info"

EXPOSE 8080

# /healthz is the one route with no auth (see docs/CONTRACT.md). Overridable
# because a custom PRIZMA_ADDR moves the port out from under a hardcoded URL.
ENV PRIZMA_HEALTH_URL="http://127.0.0.1:8080/healthz"
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
  CMD wget -q -O /dev/null "$PRIZMA_HEALTH_URL" || exit 1

ENTRYPOINT ["/usr/local/bin/prizma"]
