gitmote

gitmote  ·  history  ·  branches

perf: shrink server image to 60MB and raise container resources for faster cold start

9aeae973084fd0945fd29400ba96108e872deec7
Atanas Minev <atmin@protonmail.com> · 2026-07-06 14:52

diff --git a/Dockerfile b/Dockerfile
index 7ae000a..e56510a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,7 @@
# syntax=docker/dockerfile:1
# The build stage runs on the native BUILDPLATFORM and cross-compiles to
# TARGET* (Go is pure-Go/CGO_ENABLED=0), so a multi-arch build never emulates
-# the compiler — only the runtime stage's apt-get runs under QEMU.
+# the compiler — only the runtime stage's apk runs under QEMU.
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
@@ -13,15 +13,18 @@ RUN go mod download
COPY . .
ARG VERSION=docker
ARG TARGETOS TARGETARCH
-RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-X main.version=${VERSION}" -o /out/gitmote ./cmd/gitmote \
- && CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/gitmote-hook ./cmd/gitmote-hook
+# -s -w strips the symbol table and DWARF: ~25% smaller binaries (44M→33M for
+# gitmote), which shrinks the image and the cold-start image pull. Panics still
+# print stack traces; only gdb/delve attach is lost — irrelevant for prod.
+RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-s -w -X main.version=${VERSION}" -o /out/gitmote ./cmd/gitmote \
+ && CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-s -w" -o /out/gitmote-hook ./cmd/gitmote-hook
-# Debian's git package includes git-http-backend (Alpine's splits it out); the
-# whole design delegates to `git http-backend`, so it must be present.
-FROM debian:bookworm-slim
-RUN apt-get update \
- && apt-get install -y --no-install-recommends git ca-certificates \
- && rm -rf /var/lib/apt/lists/*
+# The whole design delegates to `git http-backend`; Alpine ships it in the
+# git-daemon package (Debian bundles it into git), so both must be installed.
+# Alpine over debian-slim cuts the runtime image from ~150MB to ~20MB of OS+git;
+# the static CGO_ENABLED=0 binaries don't care about musl vs glibc.
+FROM alpine:3.21
+RUN apk add --no-cache git git-daemon ca-certificates
# gitmote resolves the hook as gitmote-hook beside its own executable, so keep
# both in the same directory.
COPY --from=build /out/gitmote /out/gitmote-hook /usr/local/bin/
diff --git a/docs/ops.md b/docs/ops.md
index 22bd634..d52ee5d 100644
--- a/docs/ops.md
+++ b/docs/ops.md
@@ -125,14 +125,16 @@ wake, or a redeploy) the DB is restored from the derived replica and repos
re-materialize from the bucket. This path is proven locally by `make e2e-restore`
(wipes the data volume, confirms the repo still clones).
-Resources: **250 mVCPU / 512 MB / 2 GB ephemeral** (`cpu-limit=250`,
-`memory-limit-bytes=512MB` — the CLI requires a `G`/`GB`/`MB` unit). Observed
+Resources: **1000 mVCPU / 1024 MB / 8 GB ephemeral** (`cpu-limit=1000`,
+`memory-limit-bytes=1024MB` — the CLI requires a `G`/`GB`/`MB` unit). Observed
usage sits well under these; 256 MB was already plenty for `git index-pack` /
-`receive-pack`. Memory is set to 512 MB only to unlock the larger ephemeral
-scratch tier (2 GB `/tmp`), not for RAM headroom. Tune from logs.
+`receive-pack`. The CPU is set high to shorten cold start — the litestream
+restore and the first git operation run unthrottled instead of at a fraction of
+a core — and 1024 MB unlocks the larger ephemeral scratch tier (8 GB `/tmp`),
+not for RAM headroom. Tune from logs.
Scale: **`min-scale = 0`** (idle to zero). A remote pushed to occasionally is idle
-almost always, and even at 512 MB always-on exceeds Scaleway's free tier
+almost always, and even at 1024 MB always-on exceeds Scaleway's free tier
(~2.6M GB-s/mo vs 400k), so scale-to-zero is the cheaper default. The cost is a few seconds of
cold start on the first request after idle (restore + materialize, the path above);
scale-down is a graceful SIGTERM, so the shutdown `Close` durably flushes the WAL
@@ -213,15 +215,15 @@ docker push ghcr.io/atmin/gitmote-runner:master
# memory-limit-bytes, and key-based (secret-)environment-variables.KEY=value.
# Only the credentials are secret; the rest are plain environment-variables. The
# cookie key and worker secret are NOT set — they auto-generate and persist. The
-# ephemeral /tmp scratch tier is coupled to the memory tier — 512 MB unlocks 2 GB
-# scratch (set in the console).
+# ephemeral /tmp scratch tier is coupled to the memory tier — 1024 MB unlocks
+# 8 GB scratch (set in the console).
scw container container create \
namespace-id=<NAMESPACE_ID> \
name=gitmote \
image=ghcr.io/atmin/gitmote:master \
min-scale=0 max-scale=1 \
- cpu-limit=250 \
- memory-limit-bytes=512MB \
+ cpu-limit=1000 \
+ memory-limit-bytes=1024MB \
port=8080 \
environment-variables.GITMOTE_S3_BUCKET=gitmote \
environment-variables.GITMOTE_S3_ENDPOINT=https://s3.fr-par.scw.cloud \