All checks were successful
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / changes (pull_request) Successful in 8s
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-backend (pull_request) Successful in 52s
PR build (required check) / gate (pull_request) Successful in 2s
The hive-glob view must LIST every object under era5/daily/ at view bind — ~1k objects per tile, which crossed the request timeout once the lake hit 390 tiles (seen live: prod /query timing out). Bucket mode now resolves the newest Iceberg metadata JSON (one single-page LIST) and iceberg_scans it, so the file list comes from table metadata; local mode (tests, small trees) keeps the plain glob. The iceberg extension is baked next to httpfs, as the runtime user.
74 lines
3.3 KiB
Docker
74 lines
3.3 KiB
Docker
# Thermograph backend: FastAPI API + accounts/notifications + the SSR content
|
|
# JSON API frontend consumes. Split from the monorepo (repo-split Stage 7).
|
|
|
|
# thermograph-daemon (daemon/): the Go process that owns the Discord gateway
|
|
# websocket and the recurring-job timers, calling back into this app's
|
|
# /internal/* routes for anything that needs data. It is built INTO this image
|
|
# on purpose: daemon and backend share the internal API contract, so shipping
|
|
# one image (compose picks the process per-service) makes version skew between
|
|
# them impossible. CGO_ENABLED=0 gives a fully static binary that drops into
|
|
# the python:3.12-slim final stage with no runtime deps; go.mod/go.sum are
|
|
# copied and downloaded before the sources so the Go dep layer caches across
|
|
# daemon code-only changes, same reasoning as the pip layer below.
|
|
FROM golang:1.26 AS daemon-builder
|
|
WORKDIR /src
|
|
COPY daemon/go.mod daemon/go.sum ./
|
|
RUN go mod download
|
|
COPY daemon/ ./
|
|
# -trimpath keeps the build reproducible (identical sources -> identical
|
|
# binary), which is what lets the final stage's COPY layer cache-hit when only
|
|
# Python code changed.
|
|
RUN CGO_ENABLED=0 go build -trimpath -o /out/thermograph-daemon .
|
|
|
|
FROM python:3.12-slim
|
|
|
|
# curl is only for the container HEALTHCHECK below. Everything Python needs
|
|
# ships as manylinux wheels (asyncpg, psycopg[binary], polars, numpy,
|
|
# cryptography via pywebpush, PyNaCl), so no compiler/build toolchain is
|
|
# required.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends curl \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install deps first so this layer caches across code-only changes.
|
|
COPY requirements.txt /tmp/requirements.txt
|
|
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
|
|
|
# The daemon binary lands before the app tree: it changes far less often than
|
|
# the Python code, so this layer usually cache-hits and only the COPY below
|
|
# rebuilds. NOT the entrypoint — deploy/entrypoint.sh stays that; compose
|
|
# selects this binary per-service for the daemon container.
|
|
COPY --from=daemon-builder /out/thermograph-daemon /usr/local/bin/thermograph-daemon
|
|
|
|
COPY . /app/
|
|
RUN chmod +x /app/deploy/entrypoint.sh
|
|
|
|
# Non-root runtime user. Create the writable state dirs and own the whole tree
|
|
# so the parquet cache, logs, notifier.lock, homepage.json, vapid.json can be
|
|
# written. When the named volumes first mount over /app/data and /app/logs,
|
|
# Docker seeds them from this image content -- including this ownership --
|
|
# so they stay writable.
|
|
RUN useradd --system --create-home --uid 10001 thermograph \
|
|
&& mkdir -p /app/data /app/logs /state \
|
|
&& chown -R thermograph:thermograph /app /state
|
|
|
|
USER thermograph
|
|
# Bake DuckDB's httpfs + iceberg extensions AS THE RUNTIME USER — extensions
|
|
# install to the invoking user's ~/.duckdb, and a root-time bake leaves uid
|
|
# 10001 unable to LOAD them (seen live: prod lake /query 500'd on exactly
|
|
# this). iceberg powers the era5_daily view via table metadata instead of a
|
|
# bucket-wide glob LIST.
|
|
RUN python -c "import duckdb; c = duckdb.connect(); c.execute('INSTALL httpfs'); c.execute('INSTALL iceberg')"
|
|
WORKDIR /app
|
|
|
|
ENV PORT=8137 \
|
|
WORKERS=4 \
|
|
THERMOGRAPH_BASE=/ \
|
|
PYTHONUNBUFFERED=1
|
|
|
|
EXPOSE 8137
|
|
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
|
CMD curl -fsS http://127.0.0.1:${PORT}/healthz || exit 1
|
|
|
|
ENTRYPOINT ["/app/deploy/entrypoint.sh"]
|