# 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 -r -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 1.5 GiB so the runtime collects harder before the
# container's memory limit is reached. setpriv in the entrypoint preserves this
# the way it does XDG_DATA_HOME above.
ENV GOMEMLIMIT=1536MiB

# 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
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD curl -sf "http://localhost:${PORT:-8080}/.well-known/healthcheck.json" || exit 1

ENTRYPOINT ["/bin/bash", "/entrypoint.sh" ]
