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"]