build(docker): run under tini, pin alpine 3.24 and assert the stamped version in CI
- commit
- e914bbd46c2af2f7f3d946d56053ef5b9fd61018
- author
- brice · · 2026-10-10 12:08 UTC
- parent
- a87ca8b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +20 -11
M .dockerignore +4 -0
| @@ -10,3 +10,7 @@ Dockerfile | |||
| 10 | 10 | /tmp | |
| 11 | 11 | /dist | |
| 12 | 12 | tmp | |
| 13 | + | /data | |
| 14 | + | .env* | |
| 15 | + | *.pem | |
| 16 | + | *.key | |
| @@ -10,3 +10,7 @@ Dockerfile | |||||
| 10 | /tmp | 10 | /tmp | ||
| 11 | /dist | 11 | /dist | ||
| 12 | tmp | 12 | tmp | ||
| 13 | + | /data | |||
| 14 | + | .env* | |||
| 15 | + | *.pem | |||
| 16 | + | *.key | |||
M .forgejo/workflows/ci.yml +2 -1
| @@ -112,6 +112,7 @@ jobs: | |||
| 112 | 112 | # git.bnema.dev through the host's /etc/hosts (127.0.0.1, see vps-infra | |
| 113 | 113 | # homelab/igor/ci-runner) and reaches Forgejo on :8088. Podman's Docker API rejects | |
| 114 | 114 | # the classic builder's --add-host encoding, so host networking is used instead. | |
| 115 | + | # Acceptable because only trusted branches of this repo trigger CI (no fork PRs). | |
| 115 | 116 | - name: Build the image | |
| 116 | 117 | run: | | |
| 117 | 118 | docker build \ | |
| @@ -121,5 +122,5 @@ jobs: | |||
| 121 | 122 | -t ermite:ci . | |
| 122 | 123 | - name: Smoke test | |
| 123 | 124 | run: | | |
| 124 | - | docker run --rm ermite:ci version | |
| 125 | + | docker run --rm ermite:ci version | tee /dev/stderr | grep -qF "ermite $(git describe --tags --always --dirty)" | |
| 125 | 126 | docker run --rm --entrypoint git ermite:ci --version | |
| @@ -112,6 +112,7 @@ jobs: | |||||
| 112 | # git.bnema.dev through the host's /etc/hosts (127.0.0.1, see vps-infra | 112 | # git.bnema.dev through the host's /etc/hosts (127.0.0.1, see vps-infra | ||
| 113 | # homelab/igor/ci-runner) and reaches Forgejo on :8088. Podman's Docker API rejects | 113 | # homelab/igor/ci-runner) and reaches Forgejo on :8088. Podman's Docker API rejects | ||
| 114 | # the classic builder's --add-host encoding, so host networking is used instead. | 114 | # the classic builder's --add-host encoding, so host networking is used instead. | ||
| 115 | + | # Acceptable because only trusted branches of this repo trigger CI (no fork PRs). | |||
| 115 | - name: Build the image | 116 | - name: Build the image | ||
| 116 | run: | | 117 | run: | | ||
| 117 | docker build \ | 118 | docker build \ | ||
| @@ -121,5 +122,5 @@ jobs: | |||||
| 121 | -t ermite:ci . | 122 | -t ermite:ci . | ||
| 122 | - name: Smoke test | 123 | - name: Smoke test | ||
| 123 | run: | | 124 | run: | | ||
| 124 | - | docker run --rm ermite:ci version | 125 | + | docker run --rm ermite:ci version | tee /dev/stderr | grep -qF "ermite $(git describe --tags --always --dirty)" |
| 125 | docker run --rm --entrypoint git ermite:ci --version | 126 | docker run --rm --entrypoint git ermite:ci --version | ||
M Dockerfile +9 -6
| @@ -7,10 +7,12 @@ | |||
| 7 | 7 | # The modules bnema.dev/ermite-* are private and resolve through git.bnema.dev, which is | |
| 8 | 8 | # reachable from the NetBird network only. The build container uses the host's DNS and | |
| 9 | 9 | # routes, so on a NetBird host it needs nothing more. Behind a different route (CI), set | |
| 10 | - | # --build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/ [--add-host git.bnema.dev:<ip>] | |
| 10 | + | # --build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/ --network host | |
| 11 | 11 | # to fetch the modules over that URL instead of https://git.bnema.dev/. | |
| 12 | + | # | |
| 13 | + | # Base images are pinned to an Alpine minor release; bump them on purpose. | |
| 12 | 14 | ||
| 13 | - | FROM golang:1.27-alpine AS build | |
| 15 | + | FROM golang:1.27-alpine3.24 AS build | |
| 14 | 16 | RUN apk add --no-cache git | |
| 15 | 17 | ENV CGO_ENABLED=0 GOWORK=off GOTOOLCHAIN=local GOPRIVATE=bnema.dev/ermite* | |
| 16 | 18 | ARG GIT_REWRITE_BASE= | |
| @@ -25,9 +27,10 @@ RUN --mount=type=cache,target=/go/pkg/mod \ | |||
| 25 | 27 | go build -trimpath -buildvcs=false -ldflags "-s -w -X main.stamped=${VERSION}" \ | |
| 26 | 28 | -o /out/ermite ./cmd/ermite | |
| 27 | 29 | ||
| 28 | - | FROM alpine:3 | |
| 29 | - | # git-daemon provides git-http-backend on alpine. | |
| 30 | - | RUN apk add --no-cache git git-daemon ca-certificates \ | |
| 30 | + | FROM alpine:3.24 | |
| 31 | + | # git-daemon provides git-http-backend on alpine. tini runs as PID 1: it forwards signals | |
| 32 | + | # to ermite and reaps the git processes that detach from pushes (auto maintenance). | |
| 33 | + | RUN apk add --no-cache git git-daemon ca-certificates tini \ | |
| 31 | 34 | && adduser -D -u 10001 -h /home/ermite ermite \ | |
| 32 | 35 | && mkdir /data \ | |
| 33 | 36 | && chown ermite:ermite /data | |
| @@ -37,5 +40,5 @@ USER ermite | |||
| 37 | 40 | WORKDIR /data | |
| 38 | 41 | VOLUME /data | |
| 39 | 42 | EXPOSE 7341 | |
| 40 | - | ENTRYPOINT ["/usr/local/bin/ermite"] | |
| 43 | + | ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/ermite"] | |
| 41 | 44 | CMD ["serve"] | |
| @@ -7,10 +7,12 @@ | |||||
| 7 | # The modules bnema.dev/ermite-* are private and resolve through git.bnema.dev, which is | 7 | # The modules bnema.dev/ermite-* are private and resolve through git.bnema.dev, which is | ||
| 8 | # reachable from the NetBird network only. The build container uses the host's DNS and | 8 | # reachable from the NetBird network only. The build container uses the host's DNS and | ||
| 9 | # routes, so on a NetBird host it needs nothing more. Behind a different route (CI), set | 9 | # routes, so on a NetBird host it needs nothing more. Behind a different route (CI), set | ||
| 10 | - | # --build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/ [--add-host git.bnema.dev:<ip>] | 10 | + | # --build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/ --network host |
| 11 | # to fetch the modules over that URL instead of https://git.bnema.dev/. | 11 | # to fetch the modules over that URL instead of https://git.bnema.dev/. | ||
| 12 | + | # | |||
| 13 | + | # Base images are pinned to an Alpine minor release; bump them on purpose. | |||
| 12 | 14 | ||||
| 13 | - | FROM golang:1.27-alpine AS build | 15 | + | FROM golang:1.27-alpine3.24 AS build |
| 14 | RUN apk add --no-cache git | 16 | RUN apk add --no-cache git | ||
| 15 | ENV CGO_ENABLED=0 GOWORK=off GOTOOLCHAIN=local GOPRIVATE=bnema.dev/ermite* | 17 | ENV CGO_ENABLED=0 GOWORK=off GOTOOLCHAIN=local GOPRIVATE=bnema.dev/ermite* | ||
| 16 | ARG GIT_REWRITE_BASE= | 18 | ARG GIT_REWRITE_BASE= | ||
| @@ -25,9 +27,10 @@ RUN --mount=type=cache,target=/go/pkg/mod \ | |||||
| 25 | go build -trimpath -buildvcs=false -ldflags "-s -w -X main.stamped=${VERSION}" \ | 27 | go build -trimpath -buildvcs=false -ldflags "-s -w -X main.stamped=${VERSION}" \ | ||
| 26 | -o /out/ermite ./cmd/ermite | 28 | -o /out/ermite ./cmd/ermite | ||
| 27 | 29 | ||||
| 28 | - | FROM alpine:3 | 30 | + | FROM alpine:3.24 |
| 29 | - | # git-daemon provides git-http-backend on alpine. | 31 | + | # git-daemon provides git-http-backend on alpine. tini runs as PID 1: it forwards signals |
| 30 | - | RUN apk add --no-cache git git-daemon ca-certificates \ | 32 | + | # to ermite and reaps the git processes that detach from pushes (auto maintenance). |
| 33 | + | RUN apk add --no-cache git git-daemon ca-certificates tini \ | |||
| 31 | && adduser -D -u 10001 -h /home/ermite ermite \ | 34 | && adduser -D -u 10001 -h /home/ermite ermite \ | ||
| 32 | && mkdir /data \ | 35 | && mkdir /data \ | ||
| 33 | && chown ermite:ermite /data | 36 | && chown ermite:ermite /data | ||
| @@ -37,5 +40,5 @@ USER ermite | |||||
| 37 | WORKDIR /data | 40 | WORKDIR /data | ||
| 38 | VOLUME /data | 41 | VOLUME /data | ||
| 39 | EXPOSE 7341 | 42 | EXPOSE 7341 | ||
| 40 | - | ENTRYPOINT ["/usr/local/bin/ermite"] | 43 | + | ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/ermite"] |
| 41 | CMD ["serve"] | 44 | CMD ["serve"] | ||
M README.md +5 -4
| @@ -57,11 +57,12 @@ The server keeps its runtime files in `$ERMITE_DATA_DIR/run/` (the hook socket a | |||
| 57 | 57 | ||
| 58 | 58 | ```sh | |
| 59 | 59 | docker build --build-arg VERSION="$(git describe --tags --always --dirty)" -t ermite . | |
| 60 | - | docker run -d -p 7341:7341 -v ermite-data:/data \ | |
| 61 | - | -e ERMITE_PUBLIC_URL=http://localhost:7341 -e ERMITE_ADMIN_TOKEN="$(head -c 32 /dev/urandom | base64)" ermite | |
| 60 | + | token=$(head -c 32 /dev/urandom | base64) # keep it: it is the root credential | |
| 61 | + | docker run -d -p 127.0.0.1:7341:7341 -v ermite-data:/data \ | |
| 62 | + | -e ERMITE_PUBLIC_URL=http://localhost:7341 -e ERMITE_ADMIN_TOKEN="$token" ermite | |
| 62 | 63 | ``` | |
| 63 | 64 | ||
| 64 | - | The image is alpine with git, runs as uid 10001 and keeps its data in the `/data` volume. Logs are JSON by default. The build fetches `bnema.dev/ermite-*` from `git.bnema.dev`, so the host must reach it (NetBird); the build container uses the host's DNS and routes. Behind another route, pass `--build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/` (and `--add-host`), as the CI job does. | |
| 65 | + | The image is alpine with git, runs as uid 10001 and keeps its data in the `/data` volume. Logs are JSON by default. The build fetches `bnema.dev/ermite-*` from `git.bnema.dev`, so the host must reach it (NetBird); the build container uses the host's DNS and routes. Behind another route, pass `--build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/` with `--network host`, as the CI job does. | |
| 65 | 66 | ||
| 66 | 67 | ## Develop with compose | |
| 67 | 68 | ||
| @@ -69,7 +70,7 @@ The image is alpine with git, runs as uid 10001 and keeps its data in the `/data | |||
| 69 | 70 | docker compose up | |
| 70 | 71 | ``` | |
| 71 | 72 | ||
| 72 | - | Serves on `http://localhost:7341` with the development admin token in `compose.yaml`. The source is mounted and [air](https://github.com/air-verse/air) (`go tool air`, `.air.toml`) rebuilds and restarts the server on each change to a `.go` or `.templ` file; the module and build caches and the data live in named volumes. `docker compose down -v` removes them. | |
| 73 | + | Serves on `http://localhost:7341` with the development admin token in `compose.yaml`. The source is mounted and [air](https://github.com/air-verse/air) (`go tool air`, `.air.toml`) rebuilds and restarts the server on each change to a `.go` or `.templ` file; the module and build caches and the data live in named volumes. `docker compose down -v` removes them. The dev container runs as root, which maps to your user under rootless Docker. | |
| 73 | 74 | ||
| 74 | 75 | ## Build | |
| 75 | 76 | ||
| @@ -57,11 +57,12 @@ The server keeps its runtime files in `$ERMITE_DATA_DIR/run/` (the hook socket a | |||||
| 57 | 57 | ||||
| 58 | ```sh | 58 | ```sh | ||
| 59 | docker build --build-arg VERSION="$(git describe --tags --always --dirty)" -t ermite . | 59 | docker build --build-arg VERSION="$(git describe --tags --always --dirty)" -t ermite . | ||
| 60 | - | docker run -d -p 7341:7341 -v ermite-data:/data \ | 60 | + | token=$(head -c 32 /dev/urandom | base64) # keep it: it is the root credential |
| 61 | - | -e ERMITE_PUBLIC_URL=http://localhost:7341 -e ERMITE_ADMIN_TOKEN="$(head -c 32 /dev/urandom | base64)" ermite | 61 | + | docker run -d -p 127.0.0.1:7341:7341 -v ermite-data:/data \ |
| 62 | + | -e ERMITE_PUBLIC_URL=http://localhost:7341 -e ERMITE_ADMIN_TOKEN="$token" ermite | |||
| 62 | ``` | 63 | ``` | ||
| 63 | 64 | ||||
| 64 | - | The image is alpine with git, runs as uid 10001 and keeps its data in the `/data` volume. Logs are JSON by default. The build fetches `bnema.dev/ermite-*` from `git.bnema.dev`, so the host must reach it (NetBird); the build container uses the host's DNS and routes. Behind another route, pass `--build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/` (and `--add-host`), as the CI job does. | 65 | + | The image is alpine with git, runs as uid 10001 and keeps its data in the `/data` volume. Logs are JSON by default. The build fetches `bnema.dev/ermite-*` from `git.bnema.dev`, so the host must reach it (NetBird); the build container uses the host's DNS and routes. Behind another route, pass `--build-arg GIT_REWRITE_BASE=http://git.bnema.dev:8088/` with `--network host`, as the CI job does. |
| 65 | 66 | ||||
| 66 | ## Develop with compose | 67 | ## Develop with compose | ||
| 67 | 68 | ||||
| @@ -69,7 +70,7 @@ The image is alpine with git, runs as uid 10001 and keeps its data in the `/data | |||||
| 69 | docker compose up | 70 | docker compose up | ||
| 70 | ``` | 71 | ``` | ||
| 71 | 72 | ||||
| 72 | - | Serves on `http://localhost:7341` with the development admin token in `compose.yaml`. The source is mounted and [air](https://github.com/air-verse/air) (`go tool air`, `.air.toml`) rebuilds and restarts the server on each change to a `.go` or `.templ` file; the module and build caches and the data live in named volumes. `docker compose down -v` removes them. | 73 | + | Serves on `http://localhost:7341` with the development admin token in `compose.yaml`. The source is mounted and [air](https://github.com/air-verse/air) (`go tool air`, `.air.toml`) rebuilds and restarts the server on each change to a `.go` or `.templ` file; the module and build caches and the data live in named volumes. `docker compose down -v` removes them. The dev container runs as root, which maps to your user under rootless Docker. |
| 73 | 74 | ||||
| 74 | ## Build | 75 | ## Build | ||
| 75 | 76 | ||||