Files
DodoSSH/src/DodoSSH.Api/Dockerfile
T
jaap-jan 9c7be8ae9b Let the API image see the trimmer pin, so its restore stops breaking on each .NET servicing release
The API Dockerfile copied four of the five root build files before restoring and left out Directory.Build.targets, which pins Microsoft.NET.ILLink.Tasks. The image's locked-mode restore therefore asked for whatever ILLink its SDK bundled, and when sdk:10.0-noble moved to 10.0.401 (ILLink 10.0.12) it failed with NU1004 against lock files that correctly say 10.0.11 - while ci.yml, which sees the pin, stayed green.
2026-09-28 21:44:56 +02:00

146 lines
8.4 KiB
Docker

# The API image. Built from the repository root, not from this directory:
#
# docker build -f src/DodoSSH.Api/Dockerfile .
#
# Directory.Build.props, Directory.Build.targets, Directory.Packages.props, NuGet.config and
# global.json all sit at the root and all five are load-bearing here — central package
# management means a csproj alone does not name a single version, and a build that cannot see
# them resolves nothing.
#
# Directory.Build.targets is the one that is easy to forget, because leaving it out breaks
# nothing until the day it does: it pins the version of the trimmer the SDK adds to
# DodoSSH.Contracts and DodoSSH.Crypto by itself (see the comment there). Without it the restore
# below asks for whatever ILLink the image's SDK bundles, which matches the lock files only
# until .NET ships a servicing release — and then this image fails with NU1004 while ci.yml,
# which does see the pin, stays green. That is how it was found: sdk:10.0-noble moved to
# 10.0.401 (ILLink 10.0.12) and this build broke with no commit touching a package.
# ---------------------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------------------
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
# Reproducible builds. Directory.Build.props turns ContinuousIntegrationBuild on when this
# is set, which is what normalises the source paths baked into the PDBs — without it two
# builds of the same commit differ by the directory they happened in.
ENV CI=true \
DOTNET_NOLOGO=true \
DOTNET_CLI_TELEMETRY_OPTOUT=true \
DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true
WORKDIR /src
# The manifests first, and only the manifests. This layer is what makes an ordinary code
# change a ten-second rebuild instead of a full package restore: it is invalidated by a
# dependency change and by nothing else. Every project in DodoSSH.Api's reference closure
# has to be here — restore walks ProjectReference, and a missing csproj fails the graph
# rather than skipping a node.
COPY global.json NuGet.config Directory.Build.props Directory.Build.targets Directory.Packages.props ./
COPY src/DodoSSH.Api/DodoSSH.Api.csproj src/DodoSSH.Api/
COPY src/DodoSSH.Api/packages.lock.json src/DodoSSH.Api/
COPY src/DodoSSH.Contracts/DodoSSH.Contracts.csproj src/DodoSSH.Contracts/
COPY src/DodoSSH.Contracts/packages.lock.json src/DodoSSH.Contracts/
COPY src/DodoSSH.Crypto/DodoSSH.Crypto.csproj src/DodoSSH.Crypto/
COPY src/DodoSSH.Crypto/packages.lock.json src/DodoSSH.Crypto/
COPY src/DodoSSH.Domain/DodoSSH.Domain.csproj src/DodoSSH.Domain/
COPY src/DodoSSH.Domain/packages.lock.json src/DodoSSH.Domain/
COPY src/DodoSSH.Infrastructure/DodoSSH.Infrastructure.csproj src/DodoSSH.Infrastructure/
COPY src/DodoSSH.Infrastructure/packages.lock.json src/DodoSSH.Infrastructure/
# Locked mode here for the same reason CI uses it: the lock files are committed, so a
# dependency that changed without its lock file being reviewed fails the build rather than
# quietly shipping. An image is the one place that matters most.
RUN dotnet restore src/DodoSSH.Api/DodoSSH.Api.csproj --locked-mode
# BannedSymbols.txt is an AdditionalFiles entry in Directory.Build.props. Without it the
# BannedApiAnalyzers rules silently pass, and with TreatWarningsAsErrors the whole point of
# the list is that it fails a build — so its absence would be invisible in exactly the way
# it is meant to prevent.
COPY BannedSymbols.txt .editorconfig ./
COPY src/ src/
# The version, handed in rather than derived, because there is no repository in here to derive
# it from: MinVer reads git tags, and .dockerignore excludes .git/ deliberately — the context is
# the repository root and copying the whole history into every image build would be absurd.
#
# Without this the build still succeeds (MINVER1001 is a warning, and TreatWarningsAsErrors does
# not escalate a task warning), and that is the trap: the image would be built with the SDK's
# fallback version and GET /api/v1/meta would report 0.0.0-alpha.0 as its serverVersion, which is
# a lie told quietly. The tag is already parsed by the workflow for the image tags, so it is the
# same number, passed one step further.
#
# MinVerSkip because there is nothing here for it to do, and it should not warn about it either.
#
# ASSEMBLY_VERSION and emphatically not VERSION, which is the trap this block exists to avoid and
# which cost a build to find. An ARG is an environment variable for the rest of the stage, MSBuild
# reads environment variables as global properties, and property names are case-insensitive — so an
# `ARG VERSION` in a build stage silently sets MSBuild's `Version` for every project in it. With the
# workflow passing `main-<short sha>` on a main build, that is not a version the SDK will accept, and
# the publish dies with NETSDK1018 "Invalid NuGet version string" pointing at DodoSSH.Contracts, a
# project nobody changed. The name is the whole fix; the ARG in the final stage below is only ever a
# label and never meets MSBuild.
#
# The workflow passes this empty except on a tag build, so a main image keeps the SDK default rather
# than carrying a version that is not one.
ARG ASSEMBLY_VERSION=""
RUN dotnet publish src/DodoSSH.Api/DodoSSH.Api.csproj \
--no-restore \
--configuration Release \
--output /app \
-p:MinVerSkip=true \
${ASSEMBLY_VERSION:+-p:Version="$ASSEMBLY_VERSION"} \
-p:UseAppHost=false
# ---------------------------------------------------------------------------------------
# Runtime
# ---------------------------------------------------------------------------------------
#
# Chiseled: no shell, no package manager, no libc utilities, and a non-root user (uid 1654)
# already set by the base image. That closes off `docker exec sh` on a process that holds a
# database connection and the cursor signing key, and it is affordable here specifically
# because Directory.Build.props sets InvariantGlobalization — the ICU and tzdata a normal
# base carries are exactly what this product has already decided it does not use.
#
# The cost is real and worth stating: there is no HEALTHCHECK below, because there is no
# curl and nothing to run one with. The health endpoints exist and are anonymous —
# /healthz/live, /healthz/ready, /healthz/startup — so the probe belongs in whatever runs
# the container. Readiness is not decoration on this API: it fails while an EF migration is
# pending and names the one it is waiting for, which is the intended way to discover that a
# deployment shipped ahead of its schema.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS final
# Passed by CI; see .github/workflows/ci.yml. Declared with empty defaults so a local
# `docker build` with no arguments still succeeds.
ARG VERSION=""
ARG REVISION=""
ARG CREATED=""
LABEL org.opencontainers.image.title="DodoSSH API" \
org.opencontainers.image.description="DodoSSH server: sync, identity, teams and relay authorization." \
org.opencontainers.image.vendor="DodoTech" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://git.dodotech.cloud/DodoTech-Public/DodoSSH" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}"
WORKDIR /app
COPY --from=build /app .
# 8080 is the .NET container default (ASPNETCORE_HTTP_PORTS in the base image), and plain
# HTTP is deliberate: Program.cs has no UseHttpsRedirection because the API is always behind
# a proxy that terminates TLS, and redirecting from here would loop.
EXPOSE 8080
# Configuration reaches the process two ways, both already wired in Program.cs: environment
# variables prefixed DODOSSH_, and files under /run/secrets for anything that should not be
# readable in `docker inspect`. The three the process will not start or run correctly
# without are DODOSSH_ConnectionStrings__Postgres, DODOSSH_Oidc__Authority and — on more
# than one node — DODOSSH_Sync__CursorSigningKey.
#
# Nothing migrates the database from in here. That is the API's own design: it fails
# /healthz/ready while a migration is pending and names it, so the schema is applied by
# `dotnet ef database update` alongside the deployment rather than by a racing container.
ENTRYPOINT ["dotnet", "DodoSSH.Api.dll"]