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:
90
.env.example
Normal file
90
.env.example
Normal 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
6
.gitignore
vendored
@@ -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/
|
||||
|
||||
86
Dockerfile
86
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=<arch> and emit
|
||||
# into build/<arch>/, 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"]
|
||||
|
||||
23
Makefile
23
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 ./...
|
||||
|
||||
|
||||
46
README.md
46
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-<arch>`, so no
|
||||
qemu-emulated builder is involved.
|
||||
|
||||
64
docker-compose.yaml
Normal file
64
docker-compose.yaml
Normal 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"
|
||||
Reference in New Issue
Block a user