Files
commerce/Dockerfile.commerce-admin
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

74 lines
4.0 KiB
Docker

# syntax=docker/dockerfile:1
# commerce-admin — THE Commerce admin (app/admin, @hanzo/commerce-dashboard) as
# PRODUCTION serves it: the static export behind the canonical hanzoai/static
# plugin on :3000.
#
# Why this exists next to the binary's own //go:embed copy: production's commerce
# API is the hanzoai/cloud unified binary (the standalone commerce pods are
# retired — see universe infra/k8s/commerce/kustomization.yaml), and a library
# consumer CANNOT serve this export. ui/dist is gitignored build output, so the
# Go module zip cloud resolves carries .gitkeep and nothing else. "The binary
# that owns /v1 owns /admin" holds for the standalone binary (devnet, local dev);
# in the cluster the export needs a server of its own, and that server is the
# static plugin — never nginx, never a node runtime.
#
# ONE producer, ONE URL contract: the export is built by the same
# `turbo run typecheck build --filter=@hanzo/commerce-dashboard` the root
# Dockerfile's admin-build stage runs, and it is BUILT for /admin
# (next.config.ts basePath) — so it is laid out under /srv/admin here and the
# chunk URLs it emits (/admin/_next/*) resolve byte-for-byte the same way they do
# on the binary's /admin mount. A second Dockerfile rather than a second target
# of the first because hanzo.yml's image list has no `target:` key.
# ── Stage 1: build the admin export from source ───────────────────────────────
FROM node:20-slim AS admin-build
RUN npm install -g [email protected]
WORKDIR /build
COPY app/ ./app/
WORKDIR /build/app
RUN pnpm install --frozen-lockfile
# Prod defaults baked into the client bundle (Next inlines NEXT_PUBLIC_*), same
# values the root Dockerfile bakes: one bundle, whichever server ships it.
ENV NEXT_PUBLIC_COMMERCE_API_URL=https://api.hanzo.ai \
NEXT_PUBLIC_IAM_SERVER_URL=https://hanzo.id \
NEXT_PUBLIC_IAM_CLIENT_ID=hanzo-commerce \
NEXT_PUBLIC_HANZO_AI_URL=https://api.hanzo.ai/v1/chat/completions \
NEXT_PUBLIC_HANZO_AI_MODEL=best
RUN node_modules/.bin/turbo run typecheck build --filter=@hanzo/commerce-dashboard
# ── Stage 2: serve it (hanzoai/static has no shell to arrange files with) ─────
FROM busybox:1.36 AS arrange
COPY --from=admin-build /build/app/admin/out /srv/admin
# ── Stage 3: the canonical Hanzo static plugin ────────────────────────────────
# No --spa: `trailingSlash: true` gives every route its own index.html, so a real
# route resolves by directory index and a miss stays a 404 instead of answering
# 200 with HTML (the fallback that hid a broken bundle twice already).
#
# PINNED TO v0.5.1 BECAUSE IT IS THE VERSION PROVEN AGAINST THIS EXPORT.
#
# Rolling the v0.5.2 image took commerce.hanzo.ai down for the length of one
# deploy: every page came back `Content-Type: application/x-gzip` with no
# `Content-Encoding`, and this server sets `nosniff`, so the browser was handed
# what it read as a gzip download and refused to render it. The BODY was correct
# — only the framing was wrong, which is why a status check and a payload diff
# both pass while the page is blank.
#
# What that was NOT: a defect in the v0.5.2 commit. That tag differs from v0.5.1
# by one CI workflow file and no Go code, yet the two IMAGES differ by 26KB of
# binary — the published v0.5.2 image does not correspond to its own tag, so
# reading the tag diff says nothing about what it does. Check the image.
#
# The upstream has moved on to v0.5.6, and compression landed correctly on main
# after that image was cut (hanzoai/static 900bccf sets Content-Encoding, keeps
# the real Content-Type, drops Content-Length). Moving this pin FORWARD to v0.5.6
# is the right next step; skipping straight over the artifact that broke is the
# point. `framing_test.go` upstream now fails if an HTML document is ever framed
# as anything else, so the class cannot ship silently again.
FROM ghcr.io/hanzoai/static:v0.5.1
COPY --from=arrange /srv /srv
EXPOSE 3000
ENTRYPOINT ["/static", "--root=/srv", "--port=3000", "--404=admin/404.html"]