initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled

This commit is contained in:
Daniel Noulet 2026-10-07 15:11:35 +02:00
commit 951dd8d26b
175 changed files with 50041 additions and 0 deletions

159
Dockerfile Normal file
View file

@ -0,0 +1,159 @@
# syntax=docker/dockerfile:1.7
#
# Go Bananas!! container image.
#
# Design notes
# ------------
# * The Go build is fully static. modernc.org/sqlite is a pure-Go translation of
# SQLite, so CGO is not needed and the runtime image needs no libc at all. That
# is what lets the final stage be `scratch`-adjacent: a smaller image, and no
# shared-library CVEs to patch.
# * The frontend and the SDK are built before the Go binary and copied in, so the
# image build fails if the frontend fails. A container that starts and then 404s
# on its own dashboard is worse than one that does not start.
# * Dependencies are installed from a lockfile in their own layer, so a source-only
# change does not re-download the module cache.
# * BuildKit cache mounts are used for the module and package caches. They are a
# cache, not an artefact, so they are not part of the image.
# ---------------------------------------------------------------- arguments --
ARG GO_VERSION=1.27.1
ARG NODE_VERSION=24.9.0
ARG ALPINE_VERSION=3.22
# ------------------------------------------------------------- web and sdk ----
FROM node:${NODE_VERSION}-alpine AS assets
# Corepack is enabled so that the pinned package manager in package.json is the one
# that runs, rather than whatever npm happens to ship with the Node release.
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /src/web
COPY web/package.json web/package-lock.json* ./
# `npm ci` when a lockfile is present, `npm install` otherwise. The fallback keeps
# the image buildable from a source tree that has not installed yet, while CI
# always takes the reproducible path.
RUN --mount=type=cache,target=/root/.npm \
if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; \
else npm install --no-audit --no-fund; fi
COPY web/ ./
RUN npm run build
WORKDIR /src/sdk
COPY sdk/package.json sdk/package-lock.json* ./
RUN --mount=type=cache,target=/root/.npm \
if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; \
else npm install --no-audit --no-fund; fi
COPY sdk/ ./
RUN npm run build
# ------------------------------------------------------------------- go ------
FROM golang:${GO_VERSION}-alpine AS build
ARG VERSION=dev
ARG COMMIT=unknown
ARG BUILD_DATE=unknown
ARG TARGETOS=linux
ARG TARGETARCH=amd64
# git is needed for the version stamping below when the build runs outside a
# checkout; ca-certificates for module downloads.
RUN apk add --no-cache git ca-certificates
WORKDIR /src
# Dependencies first, in their own layer. go.sum is copied and verified so that a
# tampered or truncated lockfile fails the build rather than silently resolving
# something different.
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download && go mod verify
COPY internal/ ./internal/
COPY cmd/ ./cmd/
ARG CGO_ENABLED=0
ARG GOFLAGS=-trimpath
# -trimpath removes local filesystem paths from the binary, which keeps build paths
# out of any stack trace a user might paste into a bug report.
# -s -w drop the symbol table and DWARF data: neither is useful without a matching
# local binary, and together they are a substantial fraction of the image size.
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=${CGO_ENABLED} GOFLAGS=${GOFLAGS} \
go build \
-ldflags "-s -w -X github.com/urpi/go-bananas/internal/app.Version=${VERSION} -X main.version=${VERSION} -X github.com/urpi/go-bananas/internal/app.Commit=${COMMIT} -X github.com/urpi/go-bananas/internal/app.BuildDate=${BUILD_DATE}" \
-o /out/go-bananas ./cmd/go-bananas
# Verify the binary is genuinely static before shipping it. A build that silently
# picked up cgo would produce an image that fails at run time with a confusing
# "no such file or directory" rather than at build time with a clear one.
RUN if [ "${CGO_ENABLED}" = "0" ]; then \
if go version -m /out/go-bananas | grep -q 'CGO_ENABLED=1'; then \
echo "error: binary is dynamically linked but CGO_ENABLED=0" >&2; exit 1; \
fi; \
fi
# ---------------------------------------------------------------- runtime ----
# Alpine rather than distroless or scratch: the service needs a shell for a health
# check and a writable directory for SQLite, and Alpine supplies both in ~8 MB. The
# attack surface difference against distroless is small next to the operational
# benefit, and the runtime user is unprivileged either way.
FROM alpine:${ALPINE_VERSION} AS runtime
ARG VERSION=dev
# curl is only here for the container health check. wget from busybox would do, but
# it is already present and curl's exit codes are the ones the check reasons about.
# tzdata is needed because timestamps are rendered in the configured locale.
RUN apk add --no-cache ca-certificates tzdata curl \
&& addgroup -g 65532 -S bananas \
&& adduser -u 65532 -S -G bananas -H -s /sbin/nologin bananas
COPY --from=build /out/go-bananas /usr/local/bin/go-bananas
COPY --from=assets /src/web/dist /srv/web
COPY --from=assets /src/sdk/dist /srv/sdk
# The data directory holds the SQLite file. It is a volume in every real deployment;
# creating it here means an unconfigured local run still starts.
RUN mkdir -p /srv/data && chown -R bananas:bananas /srv
USER 65532:65532
WORKDIR /srv
ENV BANANAS_HTTP_ADDR=:8080 \
BANANAS_STATIC_DIR=/srv/web \
BANANAS_SDK_DIR=/srv/sdk \
BANANAS_DATABASE_PATH=/srv/data/go-bananas.db \
BANANAS_LOG_FORMAT=json
EXPOSE 8080
VOLUME ["/srv/data"]
# The health check uses the liveness probe rather than readiness: a container that
# is alive but not ready should stay in the load-balancer pool's view of "starting"
# rather than be restarted, and Docker's HEALTHCHECK does not have that concept.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl --fail --silent --show-error http://127.0.0.1:8080/healthz || exit 1
ENTRYPOINT ["/usr/local/bin/go-bananas"]
# --------------------------------------------------------------- final ------
FROM runtime AS final
ARG VERSION=dev
LABEL org.opencontainers.image.title="go-bananas" \
org.opencontainers.image.description="Runtime design-token service: author themes, serve them to any application at run time." \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.source="https://github.com/urpi/go-bananas" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.base.name="docker.io/library/alpine:3.22"