Uh oh!
There was an error while loading. Please reload this page.
feat(server): multi-stage Dockerfile that builds Go from source - #19
feat(server): multi-stage Dockerfile that builds Go from source#19Flo5k5 wants to merge 7 commits into
Conversation
The previous Dockerfile assumed that the Go binary was already built on the host and copied into the build context before running docker build. That required a separate prep step, only worked when the host arch matched the target, and made the docker.sh pre-build shell mandatory. The multi-stage version compiles the server inside the image using golang:1.24-alpine, so a plain "docker build" works out of the box on any platform supported by the builder (amd64, arm64...). This is what lets a homelab node cross-build and run the server natively on Raspberry Pi without juggling pre-compiled binaries. Also: - build with -trimpath and stripped symbols for smaller images - install ca-certificates and tzdata in the runtime layer so TLS and timezone-aware logging work correctly - EXPOSE 12800 so compose tools and scanners see the port - run the binary via absolute path to remove the implicit ./ in CMD
…26.3) Backend v2 (merged from upstream/main) bumped go.mod to 1.26.3, but the multi-stage Dockerfile (PR m5stack#19) still pinned golang:1.24-alpine. Build was failing on 'go mod download' with GOTOOLCHAIN=local.
…26.3) Backend v2 (merged from upstream/main) bumped go.mod to 1.26.3, but the multi-stage Dockerfile (PR m5stack#19) still pinned golang:1.24-alpine. Build was failing on 'go mod download' with GOTOOLCHAIN=local.
…om v2) Backend v2 (merged from upstream/main) added imports (gerror, guid, ...) without regenerating go.sum. Build failed with 'missing go.sum entry'. Adding 'go mod tidy' before 'go build' to reconcile.
Backend v2 calls s.SetServerRoot("web/management") at boot and FATAs if
the dir is missing. The admin frontend UI is built separately (npm) and
not bundled in our image. Creating an empty dir lets the API boot; the
UI itself is not needed for the robot pipeline.Backend v2 (utility/rsa.go) panics at init() if any of the 4 RSA keys
(rsa.{server,client}.{public,private}) is empty in config.yaml. This
forced operators to leak PEM material into a committed config.
Add entrypoint.sh that:
- Generates two 2048-bit RSA pairs at first boot if config.yaml has no
PEM material yet (idempotent: skips if operator pre-filled the keys).
- Persists them to /app/rsa-keys (bind-mount-friendly for stable keys
across container recreates).
- Rewrites the rsa: section in config.yaml with YAML block scalars
carrying the generated PEM.
Dockerfile:
- COPY entrypoint.sh, CMD points to it.
- mkdir /app/web/management (already done, kept).
Behavior validated locally with the upstream config template (YAML still
parses, all sections preserved, server.private filled with valid PEM).Flo5k5
commented
Aug 6, 2026
I've enriched this PR to cover the backend v2 requirements (merged via commit 4 additional fixes layered on top of the original multi-stage Dockerfile (
Bonus context I discovered along the way (heads-up, separate from this PR): Happy to split into separate PRs if that helps review. The original multi-stage goal (build Go from source instead of COPY pre-compiled binary) is preserved as the bottom commit. |
entrypoint.sh generates RSA keys via openssl at first boot, but the
runtime stage (alpine:latest) did not ship openssl. Container crashed
with exit 127 ('openssl: not found') in a loop. Adding it to the apk
install line alongside ca-certificates and tzdata.…-route bug) Backend v2 has a duplicate route bug: device.NewV1 (ControllerV1.GetUser AccountInfo) and stackchandevice.NewV2 (ControllerV2.GetDeviceUserInfo) both register GET /stackChan/device/user, FATAs at boot. Confirmed on pure upstream/main (not from our merge). Rather than patching cmd.go (would touch upstream code), inject server.routeOverWrite: true just under the top-level server: key in config.yaml at boot. Lets the second registration silently overwrite the first. Idempotent: skipped if already present.
The previous Dockerfile assumed that the Go binary was already built on
the host and copied into the build context before running docker build.
That required a separate prep step, only worked when the host arch
matched the target, and made the docker.sh pre-build shell mandatory.
The multi-stage version compiles the server inside the image using
golang:1.24-alpine, so a plain "docker build" works out of the box on
any platform supported by the builder (amd64, arm64...). This is what
lets a homelab node cross-build and run the server natively on
Raspberry Pi without juggling pre-compiled binaries.
Also:
timezone-aware logging work correctly