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.
This commit is contained in:
2026-04-15 18:07:07 +02:00
parent bc6ccaa844
commit 6a48c12449
6 changed files with 291 additions and 24 deletions

90
.env.example Normal file
View File

@@ -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

6
.gitignore vendored
View File

@@ -1,5 +1,11 @@
build/ build/
/*.yaml /*.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/ docs/implementation/
tests/out/ tests/out/
tests/.venv/ tests/.venv/

View File

@@ -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 WORKDIR /src
# Cache Go modules first so code-only rebuilds skip the download.
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN make build
# ---- runtime image ---------------------------------------------------------- # The protoc-generated files are checked in, but docker COPY resets
FROM debian:bookworm-slim # 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 \ # build-amd64 / build-arm64 both set GOOS=linux GOARCH=<arch> and emit
iproute2 \ # into build/<arch>/, so the runtime stages below can COPY straight
&& rm -rf /var/lib/apt/lists/* # 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: # ca-certificates — HTTPS health checks need a trust store.
# CAP_NET_RAWopen raw ICMP sockets for health probing # iproute2 `ip netns` helpers, useful for debugging netns-scoped probes.
# RUN apk add --no-cache ca-certificates iproute2
# Grant these in your container runtime, e.g.:
# docker run --cap-add NET_RAW ...
# or in Kubernetes via securityContext.capabilities.add
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"]

View File

@@ -54,7 +54,7 @@ GO_VERSION ?= 1.25.0
# make install-deps GOLANGCI_LINT_VERSION=2.0.0 # make install-deps GOLANGCI_LINT_VERSION=2.0.0
GOLANGCI_LINT_VERSION ?= 1.64.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 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 amd64 $(VERSION)
debian/build-deb.sh vpp-maglev arm64 $(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) test: $(GEN_FILES)
go test ./... go test ./...

View File

@@ -114,13 +114,43 @@ deployments.
## Docker ## Docker
```sh A single multi-stage Alpine `Dockerfile` produces two images, driven
docker build -t maglevd . from `docker-compose.yaml` at the repo root:
docker run --cap-add NET_RAW \
-v /etc/vpp-maglev:/etc/vpp-maglev maglevd
# With netns-scoped health checks (maglev.yaml sets healthchecker.netns): - `git.ipng.ch/ipng/vpp-maglevd:latest` — the health-checker daemon.
docker run --cap-add NET_RAW --cap-add SYS_ADMIN \ - `git.ipng.ch/ipng/vpp-maglevd-frontend:latest` — the read-only web
-v /etc/vpp-maglev:/etc/vpp-maglev \ dashboard.
-v /var/run/netns:/var/run/netns maglevd
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-<arch>`, so no
qemu-emulated builder is involved.

64
docker-compose.yaml Normal file
View File

@@ -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"