# The API image. Built from the repository root, not from this directory: # # docker build -f src/DodoSSH.Api/Dockerfile . # # Directory.Build.props, Directory.Packages.props, NuGet.config and global.json all sit at # the root and all four 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. # --------------------------------------------------------------------------------------- # 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.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/ RUN dotnet publish src/DodoSSH.Api/DodoSSH.Api.csproj \ --no-restore \ --configuration Release \ --output /app \ -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/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"]