Files
zeekayandHanzo Dev 3f2b6f515d
CI/CD / cicd (push) Failing after 1m48s
Hanzo CI/CD / cicd (push) Failing after 1m47s
The builder carries the zone data and certificates the image copies
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 03:24:42 -07:00

120 lines
5.0 KiB
Docker

# syntax=docker/dockerfile:1
# ---- Stage 1: Build WebUI dashboard ----
FROM --platform=$BUILDPLATFORM node:24-alpine3.22 AS webui
RUN corepack enable && corepack prepare [email protected] --activate
WORKDIR /src/webui
COPY webui/package.json webui/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY webui/ ./
RUN pnpm build
# ---- Stage 2: Build Go binary ----
FROM --platform=$BUILDPLATFORM golang:1.26.8-alpine AS builder
# go.mod pins the toolchain. The golang base image sets GOTOOLCHAIN=local,
# which turns a `go` directive newer than the image into a hard build
# failure instead of a download.
ENV GOTOOLCHAIN=auto
RUN apk add --no-cache git tzdata ca-certificates
ARG TARGETOS=linux
ARG TARGETARCH=amd64
ARG VERSION=v0.1.0
WORKDIR /src
# hanzoai/* is private, so it is fetched via authenticated git, bypassing the
# public proxy. gh_token is the shared docker-build.yml BuildKit secret; no-op
# when absent (local/dev).
#
# zap-proto/* is NOT private — the repos are public and proxy.golang.org serves
# them. Listing it here sent go to direct git for a module the proxy already
# had, and that path is the fragile one:
#
# reading github.com/zap-proto/go/go.mod at revision v1.3.0: git ls-remote …
# fatal: unable to access 'https://github.com/zap-proto/go/': SSL: certificate
# subject name 'dotcom.glb' does not match target hostname 'github.com'
#
# GOPRIVATE names what is actually private. Anything public goes through the
# proxy, which is faster and checksum-verified.
ENV GOPRIVATE=github.com/hanzoai/*
# Copy go.mod, go.sum, and the local replace target first for layer caching.
COPY go.mod go.sum ./
COPY pkg/config/dynamic/ext/ ./pkg/config/dynamic/ext/
RUN --mount=type=secret,id=gh_token \
if [ -s /run/secrets/gh_token ]; then \
export GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="url.https://x-access-token:$(cat /run/secrets/gh_token)@github.com/.insteadOf" \
GIT_CONFIG_VALUE_0="https://github.com/"; \
fi; \
go mod download
# Copy full source.
COPY . .
# Overlay built webui assets.
COPY --from=webui /src/webui/static ./webui/static
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOEXPERIMENT=jsonv2 CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags="-w -s \
-X github.com/hanzoai/ingress/v3/pkg/version.Version=${VERSION} \
-X github.com/hanzoai/ingress/v3/pkg/version.Codename=hanzo \
-X github.com/hanzoai/ingress/v3/pkg/version.BuildDate=$(date -u '+%Y-%m-%d_%I:%M:%S%p')" \
-o /hanzo-ingress ./cmd/ingress
# ---- Stage 3: Runtime ----
# What the scratch stage copies in place of adduser and mkdir, which it lacks.
RUN printf 'ingress:x:1000:1000::/:/sbin/nologin\n' > /etc/passwd.ingress && \
printf 'ingress:x:1000:\n' > /etc/group.ingress && \
mkdir -p /emptytmp && chmod 1777 /emptytmp
# THE IMAGE IS THE BINARY.
#
# hanzo-ingress is CGO_ENABLED=0 and statically linked, so it takes nothing from
# a host. Alpine was supplying three things and none of them survives inspection:
#
# ca-certificates and tzdata are DATA the binary reads, and they copy.
# The account is two files the kernel reads to name a uid it already enforces.
# libcap-utils existed for ONE line, `setcap cap_net_bind_service=+ep`.
#
# THE FILE CAPABILITY IS REDUNDANT AND THAT IS MEASURED, not assumed. The live
# deployment runs with securityContext capabilities `add: [NET_BIND_SERVICE],
# drop: [ALL]` — the orchestrator grants the capability to the process, so a
# capability baked into the file grants nothing it does not already have. The
# image's own ports are unprivileged anyway (8080/8443, mapped by the Service),
# which is the arrangement the line below the EXPOSE has always described.
#
# The one posture this changes is `docker run` as a non-root user binding :80
# directly, with no --cap-add. That is not how this is deployed, and the remedy
# there is --cap-add=NET_BIND_SERVICE rather than a capability in the artifact.
FROM scratch
LABEL org.opencontainers.image.source="https://github.com/hanzoai/ingress"
LABEL org.opencontainers.image.title="Hanzo Ingress"
LABEL org.opencontainers.image.description="Cloud-native reverse proxy and load balancer for Hanzo infrastructure"
# Data, read by the binary, executed by nothing.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# The account. scratch has no adduser, so the two files it would have written are
# written in the builder and copied.
COPY --from=builder /etc/passwd.ingress /etc/passwd
COPY --from=builder /etc/group.ingress /etc/group
# /tmp, owned by the account. The VOLUME below replaces it at run time; this is
# what the image holds when nothing is mounted, and scratch has no mkdir.
COPY --from=builder --chown=1000:1000 /emptytmp /tmp
COPY --from=builder /hanzo-ingress /hanzo-ingress
# Bind to unprivileged ports; K8s Service/DaemonSet maps 80->8080, 443->8443
EXPOSE 8080 8443
VOLUME ["/tmp"]
USER 1000:1000
ENTRYPOINT ["/hanzo-ingress"]