# ============================================================================= # felhom-controller Dockerfile # Multi-stage build: Go binary + minimal runtime # Supports amd64 (N100 mini PCs) and arm64 (Raspberry Pi) # ============================================================================= # --- Build stage --- # R-972 (v0.322.0): a supported, patched Go, pinned by version AND digest (no moving tag). Go 1.24 had no security fixes # left (go.dev/doc/devel/release#policy). The builder's Debian does not reach the runtime (a static CGO-free binary). FROM golang:1.27.2-trixie@sha256:47667934874913a449b96faa7ef4777fa285d9ec29126ee80ed61c45b7cccf0e AS builder ARG TARGETOS=linux ARG TARGETARCH WORKDIR /build # Cache dependencies first COPY go.mod ./ RUN go mod download || true # Copy source COPY . . # Generate go.sum and ensure all deps are fetched RUN go mod tidy # R-208: the per-build values are declared HERE, just above the only step that reads them. A RUN's # cache key includes the stage's ARG environment, so declared above `go mod download` a fresh VERSION # re-ran the module download on every build (208 cache records, each used once, ~440 MB apiece). # Pinned by TestR208_DockerfileVersionArgsSitBelowModuleDownload. ARG VERSION=dev ARG GIT_COMMIT=unknown # Build static binary RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ -ldflags="-s -w \ -X main.Version=${VERSION} \ -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ -X main.GitCommit=${GIT_COMMIT}" \ -o /build/felhom-controller \ ./cmd/controller/ # --- restic: the official release binary, pinned by version AND checksum (R-1105, v0.323.0) --- # Never again „whatever the OS ships": Debian 12 → 13 would have swapped restic 0.14.0 → 0.18.0 under the off-site code # without anyone choosing it. The version below is a decision; moving it is a re-proof (07-backup-architecture.md §6.8). # Three checks, each fails the build: restic's SHA256SUMS carries a good signature from restic's release key (vendored # in third_party/restic/, fingerprint CF8F 18F2 8445 7597 3F79 D4E1 91A6 868B D3F7 A907 — no keyserver at build # time); the download matches the published sum; and it matches the sum pinned HERE, so even a re-signed release # cannot swap the binary. Pinned by TestR1105_ResticIsPinnedOutsideTheOS. FROM debian:13.7-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f AS restic ARG TARGETARCH ARG RESTIC_VERSION=0.19.1 ARG RESTIC_SHA256_AMD64=f415415624dcc452f2a02b8c33641791a8c6d6d3b65bbb3543fcf9a25151585c ARG RESTIC_SHA256_ARM64=a5f64aaab53d51e311fa3829124c5b703f2d14cf187d8640b6be3b2b49376465 COPY third_party/restic/restic-release-key.asc /tmp/restic-release-key.asc RUN set -eu; \ apt-get update && apt-get install -y --no-install-recommends ca-certificates curl gnupg gpgv bzip2; \ case "${TARGETARCH}" in \ amd64) want="${RESTIC_SHA256_AMD64}" ;; \ arm64) want="${RESTIC_SHA256_ARM64}" ;; \ *) echo "no pinned restic checksum for architecture '${TARGETARCH}'" >&2; exit 1 ;; \ esac; \ f="restic_${RESTIC_VERSION}_linux_${TARGETARCH}.bz2"; \ url="https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}"; \ cd /tmp; \ curl -fsSL -o "$f" "$url/$f"; curl -fsSL -o SHA256SUMS "$url/SHA256SUMS"; curl -fsSL -o SHA256SUMS.asc "$url/SHA256SUMS.asc"; \ gpg --dearmor < /tmp/restic-release-key.asc > /tmp/restic-release-key.gpg; \ gpgv --keyring /tmp/restic-release-key.gpg SHA256SUMS.asc SHA256SUMS; \ grep " ${f}\$" SHA256SUMS | sha256sum -c -; \ echo "${want} ${f}" | sha256sum -c -; \ bunzip2 -c "$f" > /usr/local/bin/restic; chmod 0755 /usr/local/bin/restic; \ /usr/local/bin/restic version | grep -q "^restic ${RESTIC_VERSION} " # --- Runtime stage --- # Debian 13 (R-1105, v0.323.0), pinned by point release AND digest. It waited on Debian 12 until the off-site code's # restic assumptions were re-proven on 0.18 (07-backup-architecture.md §6.8); restic itself no longer comes from here. FROM debian:13.7-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f # Install runtime dependencies — each one is a program the controller binary runs (R-1105 re-checked every exec): # - docker-cli + compose plugin: every stack operation (below) # - ca-certificates: for HTTPS (healthchecks pings, git, the hub) # - curl: the image's HEALTHCHECK, and the docker repo key below # - gnupg: dearmors docker's repo key below # - git: catalog sync # - util-linux: findmnt (mount reads) # - rsync: data migration between storage paths # - openssh-client: restic's sftp/rclone transports shell out to `ssh`; the off-site key probe # v0.323.0 (R-1105) REMOVED, because the binary never runs them: postgresql-client and default-mysql-client (every # dump and import runs INSIDE the app's own database container — `docker exec`, appbackup/dbdump.go), sqlite3, # fdisk, e2fsprogs, parted (disk work moved to the host agent in v0.37.0), sshpass (the ssh-copy-id key install it # fed was replaced by the hub's key registrar in v0.289.0). Fewer packages, fewer known holes. RUN apt-get update && apt-get upgrade -y --no-install-recommends && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ gnupg \ git \ util-linux \ rsync \ openssh-client \ && rm -rf /var/lib/apt/lists/* COPY --from=restic /usr/local/bin/restic /usr/local/bin/restic # Install docker-cli (without daemon). The repo's codename comes from the image's own /etc/os-release, never a # hard-coded name (a hard-coded `bookworm` would have kept Debian 12's packages on a Debian 13 base). RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker.gpg \ && . /etc/os-release \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] \ https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list \ && apt-get update \ && apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin \ && rm -rf /var/lib/apt/lists/* # Create non-root user (but we'll run as root for Docker socket access) # The Docker socket requires root or docker group membership RUN mkdir -p /opt/docker/felhom-controller/data COPY --from=builder /build/felhom-controller /usr/local/bin/felhom-controller # Copy baked-in app assets (logos, screenshots) # These are synced from the felhom.eu website repo before building. # See: make sync-assets COPY assets/ /usr/share/felhom/assets/ # Health check HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -f http://localhost:8080/api/health || exit 1 EXPOSE 8080 ENTRYPOINT ["/usr/local/bin/felhom-controller"] CMD ["--config", "/opt/docker/felhom-controller/controller.yaml"]