ci: add prop-base image build pipeline
Pre-builds the runtime base (wgrib2 + g2c + cdo + gdal-bin + locale + runtime apt deps) into git.mcintire.me/graham/prop-base. The base workflow is path-filtered to fire only on Dockerfile.base / build-base.yaml changes, so version bumps stay deterministic and explicit. Tag convention: `wgrib2-<v>-g2c-<v>-<unixts>` for traceability + rollback, plus `:latest` for everyday consumption. This commit only adds the new files — the app Dockerfile + build.yaml still build wgrib2 from source. A follow-up will swap the app Dockerfile to FROM prop-base:latest once this image lands in the registry.
This commit is contained in:
parent
a18586046d
commit
8ea31e4317
2 changed files with 229 additions and 0 deletions
129
.forgejo/workflows/build-base.yaml
Normal file
129
.forgejo/workflows/build-base.yaml
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
name: Build base image
|
||||||
|
|
||||||
|
# Path-filtered so this only fires when the base Dockerfile (or this
|
||||||
|
# workflow) changes — typically a wgrib2 / g2c version bump or apt-dep
|
||||||
|
# tweak. `workflow_dispatch` lets us rebuild on demand without touching
|
||||||
|
# the file (e.g. to refresh CVE patches in the apt layer).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'Dockerfile.base'
|
||||||
|
- '.forgejo/workflows/build-base.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.mcintire.me
|
||||||
|
IMAGE_NAME: graham/prop-base
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
name: Build and push base image
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
|
# Static docker + buildx download — same dance as build.yaml /
|
||||||
|
# build-grid-rs.yaml. The runner image's bookworm apt repos have
|
||||||
|
# broken GPG signatures, so we bypass apt entirely.
|
||||||
|
- name: Install Docker CLI and buildx
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /tmp/docker.tgz \
|
||||||
|
https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz
|
||||||
|
tar xzf /tmp/docker.tgz -C /tmp
|
||||||
|
install -m 0755 /tmp/docker/docker /usr/local/bin/docker
|
||||||
|
rm -rf /tmp/docker /tmp/docker.tgz
|
||||||
|
|
||||||
|
mkdir -p /usr/libexec/docker/cli-plugins
|
||||||
|
curl -fsSL -o /usr/libexec/docker/cli-plugins/docker-buildx \
|
||||||
|
https://github.com/docker/buildx/releases/download/v0.19.3/buildx-v0.19.3.linux-amd64
|
||||||
|
chmod +x /usr/libexec/docker/cli-plugins/docker-buildx
|
||||||
|
|
||||||
|
docker --version
|
||||||
|
docker buildx version
|
||||||
|
|
||||||
|
- name: Wait for Docker daemon
|
||||||
|
run: |
|
||||||
|
sock="${DOCKER_HOST:-unix:///var/run/docker.sock}"
|
||||||
|
echo "Using DOCKER_HOST=${sock}"
|
||||||
|
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo "Docker daemon reachable after ${i}s"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting for Docker daemon... ($i/30)"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Docker daemon never became reachable — diagnostics:" >&2
|
||||||
|
ls -l /var/run/docker.sock 2>&1 || true
|
||||||
|
env | grep -i docker 2>&1 || true
|
||||||
|
docker info 2>&1 || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Generate image tag
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
# Embed the wgrib2 + g2c versions in the tag so prior bases
|
||||||
|
# remain pullable for hotfix rollbacks if a version bump
|
||||||
|
# regresses something at runtime.
|
||||||
|
WGRIB2=$(grep -E '^ARG WGRIB2_VERSION=' Dockerfile.base | head -1 | cut -d= -f2)
|
||||||
|
G2C=$(grep -E '^ARG G2C_VERSION=' Dockerfile.base | head -1 | cut -d= -f2)
|
||||||
|
TIMESTAMP=$(date +%s)
|
||||||
|
TAG="wgrib2-${WGRIB2}-g2c-${G2C}-${TIMESTAMP}"
|
||||||
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Log in to container registry
|
||||||
|
run: |
|
||||||
|
attempt=1
|
||||||
|
max_attempts=3
|
||||||
|
while : ; do
|
||||||
|
if echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
||||||
|
docker login "${{ secrets.REGISTRY_URL }}" \
|
||||||
|
-u "${{ secrets.REGISTRY_USER }}" \
|
||||||
|
--password-stdin; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||||
|
echo "docker login failed after $attempt attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
delay=$((2 ** attempt))
|
||||||
|
echo "docker login attempt $attempt failed; retrying in ${delay}s"
|
||||||
|
sleep "$delay"
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Build and push base image
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||||
|
TAG="${{ steps.tag.outputs.tag }}"
|
||||||
|
|
||||||
|
attempt=1
|
||||||
|
max_attempts=3
|
||||||
|
while : ; do
|
||||||
|
if docker buildx build \
|
||||||
|
-f Dockerfile.base \
|
||||||
|
-t "${IMAGE}:${TAG}" \
|
||||||
|
-t "${IMAGE}:latest" \
|
||||||
|
--push \
|
||||||
|
.; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||||
|
echo "docker buildx build failed after $attempt attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
delay=$((2 ** attempt))
|
||||||
|
echo "docker buildx build attempt $attempt failed; retrying in ${delay}s"
|
||||||
|
sleep "$delay"
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
100
Dockerfile.base
Normal file
100
Dockerfile.base
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# syntax=docker/dockerfile:1.6
|
||||||
|
#
|
||||||
|
# Pre-built runtime base image for the Elixir app. Contains:
|
||||||
|
# * Debian trixie-slim with locale set to en_US.UTF-8
|
||||||
|
# * Runtime apt deps for the BEAM + Erlang ssl + snmp
|
||||||
|
# * `cdo` + `gdal-bin` (~1 GB dep tree, the slowest install)
|
||||||
|
# * `wgrib2` (compiled here from source against NCEPLIBS-g2c v2.3.0)
|
||||||
|
#
|
||||||
|
# Built by `.forgejo/workflows/build-base.yaml` only when this file or
|
||||||
|
# the workflow changes. The app `Dockerfile` does `FROM prop-base` so
|
||||||
|
# everyday code pushes skip the wgrib2 compile and apt-install entirely.
|
||||||
|
#
|
||||||
|
# Bumping wgrib2 or g2c: change the ARGs below and push — the base
|
||||||
|
# workflow rebuilds and re-tags `:latest`. The app Dockerfile follows
|
||||||
|
# `:latest`, so the next app push picks it up automatically.
|
||||||
|
|
||||||
|
ARG DEBIAN_VERSION=trixie-20260316-slim
|
||||||
|
ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}"
|
||||||
|
|
||||||
|
# ---- wgrib2 build stage (cached independently inside this image) ----
|
||||||
|
FROM ${RUNNER_IMAGE} AS wgrib2-builder
|
||||||
|
|
||||||
|
ARG WGRIB2_VERSION=3.8.0
|
||||||
|
# wgrib2 3.6.0 has a memory-corruption bug exposed by HRDPS rotated
|
||||||
|
# lat/lon GRIB2 files plus -lon point extraction (denormal garbage
|
||||||
|
# values, then `free(): invalid size`). Reproduced in the production
|
||||||
|
# pod against an HRDPS sample on 2026-04-30; 3.8.0 fixes it.
|
||||||
|
# g2c v2.3.0 is the minimum wgrib2 3.8.0's CMakeLists demands via
|
||||||
|
# find_package, and the first release where BUILD_G2C defaults ON.
|
||||||
|
ARG G2C_VERSION=2.3.0
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
build-essential gfortran cmake wget ca-certificates pkg-config \
|
||||||
|
zlib1g-dev libaec-dev libpng-dev libopenjp2-7-dev
|
||||||
|
|
||||||
|
# NCEPLIBS-g2c carries the actual JPEG2000 / PNG / AEC GRIB2 decoders
|
||||||
|
# that wgrib2 dispatches to via USE_G2CLIB_LOW. Jasper is disabled
|
||||||
|
# (CVE-laden, dropped from Debian).
|
||||||
|
WORKDIR /tmp/g2c
|
||||||
|
RUN wget -q --content-disposition "https://github.com/NOAA-EMC/NCEPLIBS-g2c/archive/refs/tags/v${G2C_VERSION}.tar.gz" \
|
||||||
|
-O g2c.tar.gz \
|
||||||
|
&& tar xzf g2c.tar.gz \
|
||||||
|
&& cd NCEPLIBS-g2c-${G2C_VERSION} \
|
||||||
|
&& mkdir build && cd build \
|
||||||
|
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DUSE_PNG=ON \
|
||||||
|
-DUSE_AEC=ON \
|
||||||
|
-DUSE_OpenJPEG=ON \
|
||||||
|
-DUSE_Jasper=OFF \
|
||||||
|
&& make -j$(nproc) \
|
||||||
|
&& make install
|
||||||
|
|
||||||
|
WORKDIR /tmp/wgrib2
|
||||||
|
RUN wget -q --content-disposition "https://github.com/NOAA-EMC/wgrib2/archive/refs/tags/v${WGRIB2_VERSION}.tar.gz" \
|
||||||
|
-O wgrib2.tar.gz \
|
||||||
|
&& tar xzf wgrib2.tar.gz \
|
||||||
|
&& cd wgrib2-${WGRIB2_VERSION} \
|
||||||
|
&& mkdir build && cd build \
|
||||||
|
&& cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DUSE_AEC=ON -DUSE_G2CLIB_LOW=ON \
|
||||||
|
&& make -j$(nproc) \
|
||||||
|
&& make install \
|
||||||
|
&& strip /usr/local/bin/wgrib2
|
||||||
|
|
||||||
|
# ---- Final runtime base ----
|
||||||
|
FROM ${RUNNER_IMAGE} AS base
|
||||||
|
|
||||||
|
# Runtime apt deps. cdo + gdal-bin are split into a second RUN so a
|
||||||
|
# transient cdo install failure doesn't invalidate the smaller base
|
||||||
|
# layer above.
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
libstdc++6 openssl libncurses6 locales ca-certificates snmp \
|
||||||
|
libgfortran5 libaec0 zlib1g libpng16-16t64 libopenjp2-7
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends cdo gdal-bin
|
||||||
|
|
||||||
|
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||||
|
|
||||||
|
ENV LANG=en_US.UTF-8
|
||||||
|
ENV LANGUAGE=en_US:en
|
||||||
|
ENV LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
|
# wgrib2 binary + dynamic g2c lib. ldconfig refreshes the loader cache
|
||||||
|
# so /usr/local/lib is searched at exec time without LD_LIBRARY_PATH.
|
||||||
|
COPY --from=wgrib2-builder /usr/local/bin/wgrib2 /usr/local/bin/wgrib2
|
||||||
|
COPY --from=wgrib2-builder /usr/local/lib/libg2c.so* /usr/local/lib/
|
||||||
|
RUN ldconfig
|
||||||
Loading…
Add table
Reference in a new issue