build(docker): add release image
Multi-stage build on golang:1.27-alpine, runtime alpine with git and git-http-backend, non-root uid 10001. The version is stamped with -X main.stamped from a VERSION build argument.
- commit
- 470cf1226074a719baf63712bf1ca1adb6917a32
- author
- brice · · 2026-10-10 11:55 UTC
- parent
- 2a9866e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +66 -6
A .dockerignore +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | # Keep the build context to the sources. | |
| 2 | + | .git | |
| 3 | + | .worktrees | |
| 4 | + | .forgejo | |
| 5 | + | .air.toml | |
| 6 | + | compose.yaml | |
| 7 | + | Dockerfile | |
| 8 | + | .dockerignore | |
| 9 | + | /ermite | |
| 10 | + | /tmp | |
| 11 | + | /dist | |
| 12 | + | tmp | |
| @@ -0,0 +1,12 @@ | |||||
| 1 | + | # Keep the build context to the sources. | |||
| 2 | + | .git | |||
| 3 | + | .worktrees | |||
| 4 | + | .forgejo | |||
| 5 | + | .air.toml | |||
| 6 | + | compose.yaml | |||
| 7 | + | Dockerfile | |||
| 8 | + | .dockerignore | |||
| 9 | + | /ermite | |||
| 10 | + | /tmp | |||
| 11 | + | /dist | |||
| 12 | + | tmp | |||
A Dockerfile +41 -0
| @@ -0,0 +1,41 @@ | |||
| 1 | + | # syntax=docker/dockerfile:1 | |
| 2 | + | ||
| 3 | + | # Release image: a static ermite binary on alpine with git and git-http-backend (ermite shells out to git >= 2.45). | |
| 4 | + | # | |
| 5 | + | # docker build --build-arg VERSION="$(git describe --tags --always --dirty)" -t ermite . | |
| 6 | + | # | |
| 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 | |
| 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>] | |
| 11 | + | # to fetch the modules over that URL instead of https://git.bnema.dev/. | |
| 12 | + | ||
| 13 | + | FROM golang:1.27-alpine AS build | |
| 14 | + | RUN apk add --no-cache git | |
| 15 | + | ENV CGO_ENABLED=0 GOWORK=off GOTOOLCHAIN=local GOPRIVATE=bnema.dev/ermite* | |
| 16 | + | ARG GIT_REWRITE_BASE= | |
| 17 | + | RUN if [ -n "$GIT_REWRITE_BASE" ]; then \ | |
| 18 | + | git config --global url."$GIT_REWRITE_BASE".insteadOf https://git.bnema.dev/; \ | |
| 19 | + | fi | |
| 20 | + | WORKDIR /src | |
| 21 | + | COPY . . | |
| 22 | + | ARG VERSION= | |
| 23 | + | RUN --mount=type=cache,target=/go/pkg/mod \ | |
| 24 | + | --mount=type=cache,target=/root/.cache/go-build \ | |
| 25 | + | go build -trimpath -buildvcs=false -ldflags "-s -w -X main.stamped=${VERSION}" \ | |
| 26 | + | -o /out/ermite ./cmd/ermite | |
| 27 | + | ||
| 28 | + | FROM alpine:3 | |
| 29 | + | # git-daemon provides git-http-backend on alpine. | |
| 30 | + | RUN apk add --no-cache git git-daemon ca-certificates \ | |
| 31 | + | && adduser -D -u 10001 -h /home/ermite ermite \ | |
| 32 | + | && mkdir /data \ | |
| 33 | + | && chown ermite:ermite /data | |
| 34 | + | COPY --from=build /out/ermite /usr/local/bin/ermite | |
| 35 | + | ENV ERMITE_DATA_DIR=/data ERMITE_LOG_FORMAT=json | |
| 36 | + | USER ermite | |
| 37 | + | WORKDIR /data | |
| 38 | + | VOLUME /data | |
| 39 | + | EXPOSE 7341 | |
| 40 | + | ENTRYPOINT ["/usr/local/bin/ermite"] | |
| 41 | + | CMD ["serve"] | |
| @@ -0,0 +1,41 @@ | |||||
| 1 | + | # syntax=docker/dockerfile:1 | |||
| 2 | + | ||||
| 3 | + | # Release image: a static ermite binary on alpine with git and git-http-backend (ermite shells out to git >= 2.45). | |||
| 4 | + | # | |||
| 5 | + | # docker build --build-arg VERSION="$(git describe --tags --always --dirty)" -t ermite . | |||
| 6 | + | # | |||
| 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 | |||
| 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>] | |||
| 11 | + | # to fetch the modules over that URL instead of https://git.bnema.dev/. | |||
| 12 | + | ||||
| 13 | + | FROM golang:1.27-alpine AS build | |||
| 14 | + | RUN apk add --no-cache git | |||
| 15 | + | ENV CGO_ENABLED=0 GOWORK=off GOTOOLCHAIN=local GOPRIVATE=bnema.dev/ermite* | |||
| 16 | + | ARG GIT_REWRITE_BASE= | |||
| 17 | + | RUN if [ -n "$GIT_REWRITE_BASE" ]; then \ | |||
| 18 | + | git config --global url."$GIT_REWRITE_BASE".insteadOf https://git.bnema.dev/; \ | |||
| 19 | + | fi | |||
| 20 | + | WORKDIR /src | |||
| 21 | + | COPY . . | |||
| 22 | + | ARG VERSION= | |||
| 23 | + | RUN --mount=type=cache,target=/go/pkg/mod \ | |||
| 24 | + | --mount=type=cache,target=/root/.cache/go-build \ | |||
| 25 | + | go build -trimpath -buildvcs=false -ldflags "-s -w -X main.stamped=${VERSION}" \ | |||
| 26 | + | -o /out/ermite ./cmd/ermite | |||
| 27 | + | ||||
| 28 | + | FROM alpine:3 | |||
| 29 | + | # git-daemon provides git-http-backend on alpine. | |||
| 30 | + | RUN apk add --no-cache git git-daemon ca-certificates \ | |||
| 31 | + | && adduser -D -u 10001 -h /home/ermite ermite \ | |||
| 32 | + | && mkdir /data \ | |||
| 33 | + | && chown ermite:ermite /data | |||
| 34 | + | COPY --from=build /out/ermite /usr/local/bin/ermite | |||
| 35 | + | ENV ERMITE_DATA_DIR=/data ERMITE_LOG_FORMAT=json | |||
| 36 | + | USER ermite | |||
| 37 | + | WORKDIR /data | |||
| 38 | + | VOLUME /data | |||
| 39 | + | EXPOSE 7341 | |||
| 40 | + | ENTRYPOINT ["/usr/local/bin/ermite"] | |||
| 41 | + | CMD ["serve"] | |||
M cmd/ermite/forge.go +3 -5
| @@ -9,7 +9,6 @@ import ( | |||
| 9 | 9 | "net" | |
| 10 | 10 | "os" | |
| 11 | 11 | "path/filepath" | |
| 12 | - | "runtime/debug" | |
| 13 | 12 | "time" | |
| 14 | 13 | ||
| 15 | 14 | "github.com/bnema/zerowrap" | |
| @@ -69,11 +68,10 @@ type forge struct { | |||
| 69 | 68 | // serverVersion is the version GetVersion reports: the module version of the | |
| 70 | 69 | // build, or "devel" for an untagged one. | |
| 71 | 70 | func serverVersion() string { | |
| 72 | - | info, ok := debug.ReadBuildInfo() | |
| 73 | - | if !ok || info.Main.Version == "" || info.Main.Version == "(devel)" { | |
| 74 | - | return "devel" | |
| 71 | + | if v := buildVersion(); v != "(devel)" { | |
| 72 | + | return v | |
| 75 | 73 | } | |
| 76 | - | return info.Main.Version | |
| 74 | + | return "devel" | |
| 77 | 75 | } | |
| 78 | 76 | ||
| 79 | 77 | // newForge opens the metadata store, resets run/, checks git, reconciles the repositories, | |
| @@ -9,7 +9,6 @@ import ( | |||||
| 9 | "net" | 9 | "net" | ||
| 10 | "os" | 10 | "os" | ||
| 11 | "path/filepath" | 11 | "path/filepath" | ||
| 12 | - | "runtime/debug" | |||
| 13 | "time" | 12 | "time" | ||
| 14 | 13 | ||||
| 15 | "github.com/bnema/zerowrap" | 14 | "github.com/bnema/zerowrap" | ||
| @@ -69,11 +68,10 @@ type forge struct { | |||||
| 69 | // serverVersion is the version GetVersion reports: the module version of the | 68 | // serverVersion is the version GetVersion reports: the module version of the | ||
| 70 | // build, or "devel" for an untagged one. | 69 | // build, or "devel" for an untagged one. | ||
| 71 | func serverVersion() string { | 70 | func serverVersion() string { | ||
| 72 | - | info, ok := debug.ReadBuildInfo() | 71 | + | if v := buildVersion(); v != "(devel)" { |
| 73 | - | if !ok || info.Main.Version == "" || info.Main.Version == "(devel)" { | 72 | + | return v |
| 74 | - | return "devel" | |||
| 75 | } | 73 | } | ||
| 76 | - | return info.Main.Version | 74 | + | return "devel" |
| 77 | } | 75 | } | ||
| 78 | 76 | ||||
| 79 | // newForge opens the metadata store, resets run/, checks git, reconciles the repositories, | 77 | // newForge opens the metadata store, resets run/, checks git, reconciles the repositories, | ||
M cmd/ermite/main.go +10 -1
| @@ -16,8 +16,17 @@ func main() { | |||
| 16 | 16 | } | |
| 17 | 17 | } | |
| 18 | 18 | ||
| 19 | - | // buildVersion is the module version stamped into the binary. | |
| 19 | + | // stamped is the release version set at link time by the image build | |
| 20 | + | // (-ldflags "-X main.stamped=v0.1.0"). Empty for a plain go build, which then | |
| 21 | + | // reports the module version recorded by the Go toolchain. | |
| 22 | + | var stamped string | |
| 23 | + | ||
| 24 | + | // buildVersion is the version stamped into the binary: the link-time version | |
| 25 | + | // when set, else the module version of the build. | |
| 20 | 26 | func buildVersion() string { | |
| 27 | + | if stamped != "" { | |
| 28 | + | return stamped | |
| 29 | + | } | |
| 21 | 30 | info, ok := debug.ReadBuildInfo() | |
| 22 | 31 | if !ok || info.Main.Version == "" { | |
| 23 | 32 | return "(devel)" | |
| @@ -16,8 +16,17 @@ func main() { | |||||
| 16 | } | 16 | } | ||
| 17 | } | 17 | } | ||
| 18 | 18 | ||||
| 19 | - | // buildVersion is the module version stamped into the binary. | 19 | + | // stamped is the release version set at link time by the image build |
| 20 | + | // (-ldflags "-X main.stamped=v0.1.0"). Empty for a plain go build, which then | |||
| 21 | + | // reports the module version recorded by the Go toolchain. | |||
| 22 | + | var stamped string | |||
| 23 | + | ||||
| 24 | + | // buildVersion is the version stamped into the binary: the link-time version | |||
| 25 | + | // when set, else the module version of the build. | |||
| 20 | func buildVersion() string { | 26 | func buildVersion() string { | ||
| 27 | + | if stamped != "" { | |||
| 28 | + | return stamped | |||
| 29 | + | } | |||
| 21 | info, ok := debug.ReadBuildInfo() | 30 | info, ok := debug.ReadBuildInfo() | ||
| 22 | if !ok || info.Main.Version == "" { | 31 | if !ok || info.Main.Version == "" { | ||
| 23 | return "(devel)" | 32 | return "(devel)" | ||