check / check (push) Successful in 1m8s
`useradd -r` asks for a system user and UID 1000 is above the system range, so the image build warned. Dropping `-r` ends the warning; the user keeps UID 1000, so the owner of the data directory does not change. Model: opus-5-5
153 lines
6.3 KiB
Docker
153 lines
6.3 KiB
Docker
# In every stage ARG CACHEBUST comes right after the steps that install
|
|
# system packages and download Go's modules, or right after FROM in a
|
|
# stage with neither, and every step that lints, tests or compiles
|
|
# comes after it. script/ passes a new value on every build, which
|
|
# reruns every RUN step after it. Docker keeps the layers above it
|
|
# until the base image, their own lines or go.mod and go.sum change.
|
|
# The steps that compile mount Go's build cache, and golangci-lint run
|
|
# also mounts golangci-lint's; both survive from build to build.
|
|
|
|
# Lint phase. The image is Debian-based and has gcc, so it can check the
|
|
# cgo SQLite package without extra installs.
|
|
# golangci/golangci-lint:v2.14.0 (Go 1.27.0), 2026-10-19
|
|
FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
ARG CACHEBUST
|
|
COPY . .
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
--mount=type=cache,target=/root/.cache/golangci-lint \
|
|
golangci-lint run --config .golangci.yml ./...
|
|
|
|
# Test phase. -race needs cgo and so a C compiler, which the Debian Go
|
|
# image ships and the alpine one does not. -count=1 because the mounted
|
|
# cache also holds test results, which go test would otherwise replay.
|
|
# -short skips the live RIS Live feed test, so the suite needs no network.
|
|
# golang:1.27.2-bookworm, 2026-10-19
|
|
FROM golang@sha256:5cf287a799e6b94384bad13d16b14904c531f51ba65792237e122ce42b392f61 AS test
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
ARG CACHEBUST
|
|
COPY . .
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
go test -short -count=1 -timeout 90s -race -cover ./... || \
|
|
{ echo "--- Rerunning with -v for details ---"; \
|
|
go test -short -count=1 -timeout 90s -race -v ./...; exit 1; }
|
|
|
|
# Build stage. Nothing is wanted from either phase above; the copies
|
|
# are what make BuildKit build them first, so nothing after them can
|
|
# run unless lint and test passed.
|
|
# golang:1.27.2-bookworm, 2026-10-19
|
|
FROM golang@sha256:5cf287a799e6b94384bad13d16b14904c531f51ba65792237e122ce42b392f61 AS builder
|
|
|
|
# Install build dependencies (zstd for archive, gcc for CGO/sqlite3)
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
zstd \
|
|
gcc \
|
|
libc6-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
ARG CACHEBUST
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=test /src/go.sum /dev/null
|
|
# A tar-stream context keeps the sender's file owners, which git refuses.
|
|
RUN git config --system --add safe.directory /src
|
|
COPY . .
|
|
|
|
# Vendor dependencies for the source archive (must be after copying source)
|
|
RUN go mod vendor
|
|
|
|
# Build the binary with CGO enabled (required for sqlite3). The version the
|
|
# page footer shows is the VERSION build arg when one is given, otherwise
|
|
# `git describe --tags --always` on the .git in the build context: the tag
|
|
# on a tagged commit, tag-N-gHASH after one, the short commit when no tag is
|
|
# reachable. With .git present, a version that is still empty, dev or
|
|
# unknown fails the build: git is missing or could not read the checkout.
|
|
# Without .git the footer says unknown. The footer links to the full commit.
|
|
ARG VERSION
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
VERSION="${VERSION:-$(git describe --tags --always)}"; \
|
|
if [ -e .git ]; then \
|
|
case "$VERSION" in ""|dev|unknown) \
|
|
echo "version is '$VERSION' although .git is present" >&2; \
|
|
exit 1 ;; \
|
|
esac; \
|
|
fi; \
|
|
REVISION="$(git rev-parse --verify HEAD || echo unknown)"; \
|
|
CGO_ENABLED=1 GOOS=linux go build -o /routewatch -ldflags "\
|
|
-X git.eeqj.de/sneak/routewatch/internal/version.GitRevision=$REVISION \
|
|
-X git.eeqj.de/sneak/routewatch/internal/version.GitRevisionShort=${VERSION:-unknown}" \
|
|
./cmd/routewatch
|
|
|
|
# Create source archive with vendored dependencies
|
|
RUN tar --zstd -cf /routewatch-source.tar.zst \
|
|
--exclude='.git' \
|
|
--exclude='*.tar.zst' \
|
|
.
|
|
|
|
# Runtime stage, and the last one
|
|
# debian:bookworm-slim, 2026-09-21
|
|
FROM debian@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251
|
|
|
|
# Install runtime dependencies
|
|
# - ca-certificates: for HTTPS connections
|
|
# - curl: for health checks
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
ca-certificates \
|
|
curl \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
ARG CACHEBUST
|
|
|
|
# Create non-root user
|
|
RUN useradd -u 1000 -m routewatch
|
|
|
|
RUN mkdir -p /var/lib/berlin.sneak.app.routewatch && chown routewatch:routewatch /var/lib/berlin.sneak.app.routewatch
|
|
|
|
RUN mkdir /app
|
|
WORKDIR /app
|
|
|
|
# Copy binary and source archive from builder
|
|
COPY --from=builder /routewatch /app/routewatch
|
|
COPY --from=builder /routewatch-source.tar.zst /app/source/routewatch-source.tar.zst
|
|
|
|
# Set ownership
|
|
RUN chown -R routewatch:routewatch /app
|
|
|
|
ENV XDG_DATA_HOME=/var/lib
|
|
|
|
# Cap the Go heap at 2.5 GiB so the runtime collects harder before the
|
|
# container's memory limit is reached. A full routing table takes about 1 GB
|
|
# of it (README "Memory"). setpriv in the entrypoint preserves this the way it
|
|
# does XDG_DATA_HOME above.
|
|
ENV GOMEMLIMIT=2560MiB
|
|
|
|
# Cap glibc's malloc arenas. The SQLite C library allocates and frees millions
|
|
# of small page-cache chunks from many threads; glibc otherwise creates up to
|
|
# eight arenas per core (hundreds on a large host) and keeps each arena's freed
|
|
# chunks resident, so process RSS climbs far above SQLite's live heap and never
|
|
# comes back down. Two arenas keep that retained memory bounded; database writes
|
|
# are already serialized, so the lost allocator concurrency costs nothing here.
|
|
ENV MALLOC_ARENA_MAX=2
|
|
|
|
# Expose HTTP port
|
|
EXPOSE 8080
|
|
|
|
COPY ./entrypoint.sh /entrypoint.sh
|
|
|
|
# Health check using the health endpoint, on the port PORT names. During the
|
|
# start period, Docker 25 and later checks every 5 seconds, and the first pass
|
|
# makes the container healthy. After it, Docker checks every 30 seconds. The
|
|
# start period covers the 60 seconds after which upaas reads the health
|
|
# (README "Running under upaas"). Startup reads the routing table first,
|
|
# which takes about 25 seconds when the table is full.
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
|
CMD curl -sf "http://localhost:${PORT:-8080}/.well-known/healthcheck.json" || exit 1
|
|
|
|
ENTRYPOINT ["/bin/bash", "/entrypoint.sh" ]
|