Files
ersatztv/docker/Dockerfile
T
timothyandClaude Opus 4.8 04e3a5249d
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 16s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m43s
ci(406): disable swap on the capped mysql; fix peak-step placement and failure mode [decisions-edit]
Review fixes from the cold adversarial pass on PR #411. Verdict was MERGEABLE, but one
HIGH is a genuine defect worth the round-trip.

[decisions-edit] rationale: the 12 "modified" lines in docs/decisions.md are lines THIS PR
added in its own previous commit (e33205e8), not a settled entry on main. The hook diffs
against HEAD; net against origin/main this file is still a pure insertion (65 added, 0
deleted -- verified, and CI's decisions-guard diffs against the PR base). It is also the
sanctioned reason regardless: the entry as first written contained a factual error (see
HIGH below).

HIGH -- `--memory=2g` alone silently grants 2g of SWAP as well. Docker defaults an unset
--memory-swap to 2x --memory. Verified live on bumblebee: `--memory=2g` alone gives
memory.max=2147483648 AND memory.swap.max=2147483648; adding `--memory-swap=2g` gives
memory.swap.max=0. Capping RAM while permitting swap is close to the worst outcome on the
one host whose swap thrash is the entire reason for the cap -- and a swapping mysqld
mid-DDL is exactly the pathology behind the known `Command Timeout expired` migrations
flake, so the naive cap could have made that flake worse rather than better. The comment
promising "if this ever OOM-kills the service" described a failure mode that would not
have happened first: it would have swapped, silently. Now --memory-swap=2g (swap off);
prefer a loud OOM over silent degradation.

The same 2x applies to the runners' 10g job slots (10 GiB RAM + 10 GiB swap each), which
is a plausible direct mechanism for the incident's 21 GiB swapped. That is #604's
boundary -- reporting it there.

MEDIUM -- `if: always()` does not make a step advisory. It controls whether the step RUNS,
not whether its failure fails the job, and defaults.run.shell: bash means -e is on, so a
failed cat/redirect in an informational step could redden a green test job. Use
continue-on-error: true, matching the neighbouring Coverage summary step.

MEDIUM -- the peak step must run LAST. memory.peak read at step N reports the peak only up
to N, so sitting before Coverage summary excluded reportgenerator's memory -- under-
reporting the very number #604 sizes caps on. Moved after it.

LOW -- dependency-scan.yml gets the env too; workflow env does not cross workflow files,
and that one runs unattended on a cron against the prod media host.

NIT -- Dockerfile ENV moved below the openapi-generator wget: an ENV invalidates every
layer under it, and that ~30MB download is deliberately ordered early to stay cached
(#190). Nothing between it and restore compiles, so this placement is free.

NIT -- docs no longer imply this shrinks `format`. dotnet format loads Roslyn in-process
via MSBuildWorkspace and never spawns csc, so its 3.95 GiB is untouched; sizing the small
lane on the assumption it shrank would be a trap.

Also record honestly that 543 MiB is init+idle rather than the 787-migration replay (so 2g
is a measured floor + headroom, not a measured ceiling -- the migrations job going green
is what validates it) and that --cpus=2 is judgement with no measurement behind it. The
entry criticises dressing estimates as measurements; it should not do that itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:36:18 +02:00

110 lines
5.3 KiB
Docker

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-amd64 AS dotnet-runtime
FROM node:22-bookworm-slim AS web-build
WORKDIR /source
COPY web/package*.json ./web/
WORKDIR /source/web
RUN npm ci
WORKDIR /source
COPY design-system/. ./design-system/
COPY web/. ./web/
WORKDIR /source/web
RUN npm run lint && npm run typecheck && npm test -- --run && npm run build
FROM --platform=linux/amd64 192.168.1.95:3000/timothy/ersatztv-ffmpeg:8.1.2 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip && \
python3 -m pip install --target=/app/pythonlibs --no-cache-dir streamlink && \
apt-get clean -y && \
rm -rf /var/lib/apt/lists/*
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64 AS build
RUN apt-get update && apt-get install -y ca-certificates gnupg default-jre-headless python3-pip
WORKDIR /source
# download the openapi-generator jar first so this layer is cached independently
# of the openapi spec dir below -- otherwise every spec change (e.g. v1.json)
# busts the layer and re-downloads the ~30MB jar (ersatztv#190)
RUN wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.15.0/openapi-generator-cli-7.15.0.jar
# generate openapi client
COPY ErsatzTV/wwwroot/openapi/. /app/ErsatzTV/wwwroot/openapi/
RUN java -jar openapi-generator-cli-7.15.0.jar generate -i /app/ErsatzTV/wwwroot/openapi/scripted-schedule.json -g python -o /app/etv-client --package-name etv_client
RUN rm -rf openapi-generator-cli-7.15.0.jar /app/ErsatzTV
RUN python3 -m pip install --target=/app/pythonlibs /app/etv-client
RUN rm -rf /app/etv-client
COPY scripts/scripted-schedules/. /app/scripted-schedules/
# copy csproj and restore as distinct layers
COPY *.sln .
# repo-wide build config (MSBuild props/targets incl. NuGet-audit warning
# exemptions, SDK pin, analyzer severities, and the Central Package Management
# version manifest) must be present before restore so the image build matches
# local/CI builds. Directory.Packages.props is REQUIRED here: with CPM the csproj
# carry no versions, so restore fails without the central manifest.
COPY Directory.Build.props Directory.Build.targets Directory.Packages.props global.json .editorconfig ./
COPY eng/analyzers/sdk-all-suggestion.globalconfig ./eng/analyzers/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
COPY ErsatzTV.Core.Nullable/*.csproj ./ErsatzTV.Core.Nullable/
COPY ErsatzTV.FFmpeg/*.csproj ./ErsatzTV.FFmpeg/
COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/
COPY ErsatzTV.Infrastructure.Sqlite/*.csproj ./ErsatzTV.Infrastructure.Sqlite/
COPY ErsatzTV.Infrastructure.MySql/*.csproj ./ErsatzTV.Infrastructure.MySql/
COPY ErsatzTV.Scanner/*.csproj ./ErsatzTV.Scanner/
# Disable the persistent Roslyn/MSBuild compiler servers for the in-image build (ersatztv#406,
# server-management#604). The workflow sets these as env for the runner-side dotnet jobs, but this
# stage compiles inside `docker build`, so the workflow's env does NOT reach it — the restore and
# the two publishes below would otherwise spin up their own VBCSCompiler and hold its heap. This is
# the `build` job that server-management#570 measured pegging 5.999/6 GiB, so it is the one that
# most needs this.
#
# Placed here rather than at the top of the stage on purpose: an ENV invalidates every layer below
# it, and the wget of the ~30MB openapi-generator jar above is deliberately ordered early to stay
# cached (ersatztv#190). Nothing between that wget and this line compiles, so this is the earliest
# point where the ENV is free.
#
# Build-stage only: the final image is FROM runtime-base and only COPY --from=build /app (files,
# not ENV), so none of this lands in the shipped image or affects runtime.
ENV UseSharedCompilation=false \
DOTNET_CLI_USE_MSBUILD_SERVER=0 \
MSBUILDDISABLENODEREUSE=1
RUN dotnet restore -r linux-x64 ErsatzTV/ErsatzTV.csproj
# copy everything else and build app
COPY ErsatzTV/. ./ErsatzTV/
COPY --from=web-build /source/ErsatzTV/wwwroot/app/. ./ErsatzTV/wwwroot/app/
COPY ErsatzTV.Application/. ./ErsatzTV.Application/
COPY ErsatzTV.Core/. ./ErsatzTV.Core/
COPY ErsatzTV.Core.Nullable/. ./ErsatzTV.Core.Nullable/
COPY ErsatzTV.FFmpeg/. ./ErsatzTV.FFmpeg/
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
COPY ErsatzTV.Infrastructure.Sqlite/. ./ErsatzTV.Infrastructure.Sqlite/
COPY ErsatzTV.Infrastructure.MySql/. ./ErsatzTV.Infrastructure.MySql/
COPY ErsatzTV.Scanner/. ./ErsatzTV.Scanner/
ARG INFO_VERSION="unknown"
ARG BUILD_CONFIG="release"
WORKDIR /source/ErsatzTV.Scanner
RUN dotnet publish ErsatzTV.Scanner.csproj -c ${BUILD_CONFIG} -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
WORKDIR /source/ErsatzTV
RUN sed -i '/Scanner/d' ErsatzTV.csproj
RUN dotnet publish ErsatzTV.csproj -c ${BUILD_CONFIG} -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
# final stage/image
FROM runtime-base
ENV FONTCONFIG_PATH=/etc/fonts
RUN fc-cache update
WORKDIR /app
COPY --from=build /app ./
ENV PYTHONPATH=/app/pythonlibs
ENV ETV_CONFIG_FOLDER=/config
ENV ETV_TRANSCODE_FOLDER=/transcode
ENV ETV_DISABLE_VULKAN=1
ENTRYPOINT ["./ErsatzTV"]