From 6a48c12449ebcbcd8cf38f914a2a98b91dedbc3b Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Wed, 15 Apr 2026 18:07:07 +0200 Subject: [PATCH] Add multi-arch Docker build and docker-compose stack Introduce a multi-stage Alpine Dockerfile that cross-compiles via buildx ($BUILDPLATFORM -> $TARGETARCH) so a single invocation produces both linux/amd64 and linux/arm64 images without a qemu-emulated builder. `make docker` loads the native-arch image locally for smoke tests; `make docker-push` publishes a multi-arch manifest. Ship a docker-compose.yaml with opt-in profiles for maglevd/frontend and a .env.example template so operators can mirror /etc/default/vpp-maglev muscle memory into containers. --- .env.example | 90 +++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 6 +++ Dockerfile | 86 +++++++++++++++++++++++++++++++++++-------- Makefile | 23 +++++++++++- README.md | 46 +++++++++++++++++++---- docker-compose.yaml | 64 ++++++++++++++++++++++++++++++++ 6 files changed, 291 insertions(+), 24 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d7a14b7 --- /dev/null +++ b/.env.example @@ -0,0 +1,90 @@ +# .env.example — docker-compose.yaml environment for vpp-maglev{d,-frontend} +# +# Copy to .env and edit. `.env` is gitignored so credentials stay local. +# +# cp .env.example .env +# $EDITOR .env +# docker compose up -d + +# --------------------------------------------------------------------------- +# Which containers start +# --------------------------------------------------------------------------- +# Docker Compose "profiles" decide which services actually start when you +# run `docker compose up`. Set COMPOSE_PROFILES below to a comma-separated +# list of the services you want. Valid values: +# +# maglevd — the health-checker daemon (needs VPP on the host) +# frontend — the read-only web dashboard +# +# Leave empty (or delete the line) to start nothing. The two services +# are fully independent — you can run just the frontend (IPng's default), +# just the daemon, or both. +# +# IPng default: frontend only, connecting to remote maglevds. +COMPOSE_PROFILES=frontend + +# Examples: +#COMPOSE_PROFILES=maglevd,frontend # both (typical single-host deploy) +#COMPOSE_PROFILES=maglevd # daemon only (headless fleet member) +#COMPOSE_PROFILES= # nothing + +# --------------------------------------------------------------------------- +# maglevd — the health-checker daemon +# --------------------------------------------------------------------------- +# The variable names below mirror /etc/default/vpp-maglev on a Debian +# install, so an operator moving between bare-metal and containers can +# reuse muscle memory. Each variable is read by the daemon via a +# stringFlag(..., MAGLEV_*, ...) fallback in cmd/server/main.go. + +# Path to the YAML config INSIDE the container. Mount your maglev.yaml +# at this path — the shipped docker-compose.yaml mounts ./maglev.yaml +# from the repo root, so drop your file there or edit the volume line. +MAGLEV_CONFIG=/etc/vpp-maglev/maglev.yaml + +# gRPC listen address inside the container. The compose file publishes +# port 9090 regardless; keep this as :9090 unless you know why. +MAGLEV_GRPC_ADDR=:9090 + +# Prometheus /metrics listen address. Empty string disables it. +MAGLEV_METRICS_ADDR=:9091 + +# VPP binary-API and stats sockets. The compose file bind-mounts +# /run/vpp from the host so these resolve when the daemon runs on a +# host that also runs VPP. On a host without VPP the maglevd profile +# should not be activated — but if it is, maglevd will log repeated +# reconnect failures and the LB reconciler will stay idle. +MAGLEV_VPP_API_ADDR=/run/vpp/api.sock +MAGLEV_VPP_STATS_ADDR=/run/vpp/stats.sock + +# Log verbosity: debug, info, warn, error. +MAGLEV_LOG_LEVEL=info + +# --------------------------------------------------------------------------- +# maglevd-frontend — the read-only web dashboard +# --------------------------------------------------------------------------- + +# Comma-separated list of maglevd gRPC addresses. Two common cases: +# +# - Same-host stack (COMPOSE_PROFILES=maglevd,frontend): the frontend +# reaches the daemon via Docker's internal DNS, so "maglevd:9090" +# works out of the box. +# +# - Frontend-only host (IPng case): list one or more remote maglevd +# addresses, e.g. "chbtl2.ipng.ch:9090,chlzn1.ipng.ch:9090". +MAGLEV_FRONTEND_SERVERS=maglevd:9090 + +# HTTP bind address inside the container. The compose file publishes +# port 8080 regardless. +MAGLEV_FRONTEND_LISTEN=:8080 + +# Log verbosity: debug, info, warn, error. +MAGLEV_FRONTEND_LOG_LEVEL=info + +# Optional basic-auth credentials for the /admin/ surface. When BOTH +# are set and non-empty, /admin/ is reachable and the SPA exposes +# backend lifecycle mutations (pause, resume, enable, disable, +# set-weight). When either is missing, /admin/ returns 404 and the +# SPA hides the admin toggle entirely. Leave commented for a strictly +# read-only deployment. +#MAGLEV_FRONTEND_USER=admin +#MAGLEV_FRONTEND_PASSWORD=changeme diff --git a/.gitignore b/.gitignore index ce0f71c..fee2342 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ build/ /*.yaml +# docker-compose.yaml is an exception to the /*.yaml rule above; it's +# tracked as part of the container deployment. +!/docker-compose.yaml +# .env holds (potentially secret) docker-compose runtime settings and +# must never be committed. .env.example is the tracked template. +.env docs/implementation/ tests/out/ tests/.venv/ diff --git a/Dockerfile b/Dockerfile index 86653e3..bdadefd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,82 @@ -FROM golang:1.25 AS builder +# syntax=docker/dockerfile:1.6 +# +# Multi-stage, multi-arch build for vpp-maglev container images. +# +# Produces two runtime images from a single Alpine-based builder: +# --target maglevd -> git.ipng.ch/ipng/vpp-maglevd:latest +# --target frontend -> git.ipng.ch/ipng/vpp-maglevd-frontend:latest +# +# The builder stage runs on the host's native arch ($BUILDPLATFORM) and +# Go cross-compiles to the image's target arch ($TARGETARCH), so a +# single `docker buildx build --platform linux/amd64,linux/arm64` +# produces a proper multi-arch manifest without a qemu-emulated +# builder. Both BUILDPLATFORM and TARGETARCH are populated +# automatically by buildx. +# +# Both images are driven by docker-compose.yaml at the repo root; see +# README.md and .env.example. + +# ============================================================================= +# Builder — compiles all four binaries against golang:alpine. +# ============================================================================= +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder + +# TARGETARCH is supplied by buildx (amd64, arm64, ...). Declared here +# so the make invocation below can reference it. +ARG TARGETARCH + +# make drives the Go build; nodejs/npm are needed for the SolidJS SPA +# that maglevd-frontend embeds at link time via //go:embed; git is used +# by the Makefile's `git rev-parse --short HEAD` to stamp the commit +# hash into the binary via -ldflags, and golang:alpine doesn't ship it. +RUN apk add --no-cache make nodejs npm git WORKDIR /src + +# Cache Go modules first so code-only rebuilds skip the download. COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN make build -# ---- runtime image ---------------------------------------------------------- -FROM debian:bookworm-slim +# The protoc-generated files are checked in, but docker COPY resets +# their mtimes so make may think they need regenerating. Touch them +# to ensure they are newer than the proto file and skip the rule. +RUN touch internal/grpcapi/maglev.pb.go internal/grpcapi/maglev_grpc.pb.go -RUN apt-get update && apt-get install -y --no-install-recommends \ - iproute2 \ - && rm -rf /var/lib/apt/lists/* +# build-amd64 / build-arm64 both set GOOS=linux GOARCH= and emit +# into build//, so the runtime stages below can COPY straight +# out of build/$TARGETARCH/. CGO_ENABLED=0 is exported by the Makefile. +RUN make build-$TARGETARCH -COPY --from=builder /src/bin/maglevd /usr/local/bin/maglevd +# ============================================================================= +# Runtime: maglevd (health-checker daemon) +# ============================================================================= +FROM alpine:3 AS maglevd +ARG TARGETARCH -# Required capabilities: -# CAP_NET_RAW — open raw ICMP sockets for health probing -# -# Grant these in your container runtime, e.g.: -# docker run --cap-add NET_RAW ... -# or in Kubernetes via securityContext.capabilities.add +# ca-certificates — HTTPS health checks need a trust store. +# iproute2 — `ip netns` helpers, useful for debugging netns-scoped probes. +RUN apk add --no-cache ca-certificates iproute2 -ENTRYPOINT ["/usr/local/bin/maglevd"] +COPY --from=builder /src/build/$TARGETARCH/maglevd /usr/sbin/maglevd + +# gRPC control plane (9090) and Prometheus /metrics (9091). The actual +# listen addresses are set by MAGLEV_GRPC_ADDR / MAGLEV_METRICS_ADDR. +EXPOSE 9090 9091 + +ENTRYPOINT ["/usr/sbin/maglevd"] + +# ============================================================================= +# Runtime: maglevd-frontend (read-only dashboard + optional /admin/) +# ============================================================================= +FROM alpine:3 AS frontend +ARG TARGETARCH + +RUN apk add --no-cache ca-certificates + +COPY --from=builder /src/build/$TARGETARCH/maglevd-frontend /usr/sbin/maglevd-frontend + +EXPOSE 8080 + +ENTRYPOINT ["/usr/sbin/maglevd-frontend"] diff --git a/Makefile b/Makefile index 10c0e1f..60694ae 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ GO_VERSION ?= 1.25.0 # make install-deps GOLANGCI_LINT_VERSION=2.0.0 GOLANGCI_LINT_VERSION ?= 1.64.0 -.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb robot-test clean maglevd-frontend-web install-deps install-deps-apt install-deps-go install-deps-go-tools +.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb docker docker-push robot-test clean maglevd-frontend-web install-deps install-deps-apt install-deps-go install-deps-go-tools all: build @@ -93,6 +93,27 @@ pkg-deb: build-amd64 build-arm64 debian/build-deb.sh vpp-maglev amd64 $(VERSION) debian/build-deb.sh vpp-maglev arm64 $(VERSION) +# docker — build both container images for the current host arch and +# load them into the local docker daemon. Uses buildx so the Dockerfile +# can cross-compile via $TARGETARCH; --load only supports a single +# platform, so this target is for local smoke tests. Use docker-push +# for a true multi-arch manifest. Each image is tagged both :v$(VERSION) +# and :latest in one build, so bumping VERSION is the only change +# needed to cut a new release — no hand-edited tag lists to forget. +docker: + docker buildx build --load --target maglevd -t git.ipng.ch/ipng/vpp-maglevd:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd:latest . + docker buildx build --load --target frontend -t git.ipng.ch/ipng/vpp-maglevd-frontend:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd-frontend:latest . + +# docker-push — build a multi-arch (linux/amd64,linux/arm64) manifest +# for both images and push it straight to the registry, tagged both +# :v$(VERSION) and :latest. Buildx won't --load a multi-platform +# result into the local daemon, so push is the only way to +# materialise the combined manifest. Assumes the caller is already +# logged in to git.ipng.ch. +docker-push: + docker buildx build --platform linux/amd64,linux/arm64 --push --target maglevd -t git.ipng.ch/ipng/vpp-maglevd:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd:latest . + docker buildx build --platform linux/amd64,linux/arm64 --push --target frontend -t git.ipng.ch/ipng/vpp-maglevd-frontend:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd-frontend:latest . + test: $(GEN_FILES) go test ./... diff --git a/README.md b/README.md index 5ce978b..5a83c3f 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,43 @@ deployments. ## Docker -```sh -docker build -t maglevd . -docker run --cap-add NET_RAW \ - -v /etc/vpp-maglev:/etc/vpp-maglev maglevd +A single multi-stage Alpine `Dockerfile` produces two images, driven +from `docker-compose.yaml` at the repo root: -# With netns-scoped health checks (maglev.yaml sets healthchecker.netns): -docker run --cap-add NET_RAW --cap-add SYS_ADMIN \ - -v /etc/vpp-maglev:/etc/vpp-maglev \ - -v /var/run/netns:/var/run/netns maglevd +- `git.ipng.ch/ipng/vpp-maglevd:latest` — the health-checker daemon. +- `git.ipng.ch/ipng/vpp-maglevd-frontend:latest` — the read-only web + dashboard. + +Both services are **opt-in** via Docker Compose profiles, so the same +stack file works for operators who want the daemon only, the frontend +only (IPng's own deployment), or both on one host. Copy the example +env file, choose which services to run, and start the stack: + +```sh +cp .env.example .env +$EDITOR .env # set COMPOSE_PROFILES and any overrides +docker compose up -d # starts whichever profiles are active ``` + +Valid `COMPOSE_PROFILES` values are `maglevd`, `frontend`, or both +comma-separated. Leaving it empty starts nothing. The daemon +container runs with all capabilities granted (`cap_add: ALL`) so ICMP +probes and `netns`-scoped probes both work without re-plumbing the +container; the frontend runs with no extra privileges. The `MAGLEV_*` +variables in `.env.example` mirror `/etc/default/vpp-maglev` on a +Debian install, so muscle memory carries over between the two +deployment modes. + +Build or push the images: + +```sh +make docker # buildx --load, native arch only (local smoke test) +make docker-push # buildx --push linux/amd64,linux/arm64 multi-arch manifest +``` + +`make docker` loads a single-arch image into the local daemon so you +can run it immediately; `make docker-push` produces a true multi-arch +manifest and pushes it to `git.ipng.ch/ipng/...`. Both use `docker +buildx`, and the Dockerfile cross-compiles from the host's +`$BUILDPLATFORM` to each `$TARGETARCH` via `make build-`, so no +qemu-emulated builder is involved. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..a324987 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,64 @@ +# docker-compose.yaml — vpp-maglev{d,-frontend} container stack +# +# Two services built from a single multi-stage Alpine Dockerfile: +# +# maglevd -> git.ipng.ch/ipng/vpp-maglevd:latest (health-checker daemon) +# frontend -> git.ipng.ch/ipng/vpp-maglevd-frontend:latest (read-only web dashboard) +# +# Both services are opt-in via Docker Compose "profiles". Copy +# .env.example to .env, set COMPOSE_PROFILES to include the +# services you want, and run: +# +# docker compose up -d +# +# See .env.example for every tunable. See README.md for the +# operational overview. + +services: + maglevd: + profiles: [maglevd] + build: + context: . + dockerfile: Dockerfile + target: maglevd + image: git.ipng.ch/ipng/vpp-maglevd:latest + container_name: vpp-maglevd + restart: unless-stopped + + # maglevd needs CAP_NET_RAW for ICMP probes and CAP_SYS_ADMIN for + # netns-scoped probes (see docs/design.md NFR-4.1). Granting ALL + # keeps the container operationally identical to a bare-metal + # maglevd running under the Debian systemd unit, and lets + # operators flip healthchecker.netns without re-plumbing the + # container's capability set. + cap_add: + - ALL + + # The daemon reads these via the same env-var fallback that the + # systemd unit uses — see debian/default.vpp-maglev. + env_file: .env + + # Mount the config and VPP's runtime sockets. The /run/vpp mount + # is only meaningful when the container runs on a host that also + # runs VPP; on a frontend-only host the maglevd profile is not + # activated and this block is irrelevant. + volumes: + - ./maglev.yaml:/etc/vpp-maglev/maglev.yaml:ro + - /run/vpp:/run/vpp + + ports: + - "9090:9090" # gRPC control plane + - "9091:9091" # Prometheus /metrics + + frontend: + profiles: [frontend] + build: + context: . + dockerfile: Dockerfile + target: frontend + image: git.ipng.ch/ipng/vpp-maglevd-frontend:latest + container_name: vpp-maglevd-frontend + restart: unless-stopped + env_file: .env + ports: + - "8080:8080"