ASP.NET Core Web API · Quality & Delivery

Docker

A container image packages the ShopSmart API with exactly the runtime it needs, so it runs identically on a laptop, in CI and in Azure Container Apps or Kubernetes. APIs have no views or static assets, which makes their images especially small.

Advanced 5 min Lesson 34 of 37Examples target .NET 9.
0/37 in ASP.NET Core Web API
On this page · 5 min · Advanced

In simple words#

Packaging the API in a Docker container is putting the takeaway kitchen in a food truck that can park and serve anywhere.

Dockerfile
Build recipe.
Image tag
Version label, e.g. 1.4.0.
Environment variables
Configure per environment.

Learning objectives#

  • Write a multi-stage Dockerfile for an API
  • Choose a small, secure runtime image
  • Configure ports, environment and health checks
  • Run the API with SQL Server in Docker Compose

Explanation#

Build with the SDK image and run with the ASP.NET Core runtime image. Restore first (copy only the .csproj), then copy the source and publish — Docker caches the restore layer.

Chiseled Ubuntu images (mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled) contain only what .NET needs: no shell, no package manager, non-root by default — smaller and with fewer vulnerabilities.

Images listen on port 8080 by default (ASPNETCORE_HTTP_PORTS). Terminate TLS at the ingress or load balancer; pass configuration with environment variables (ConnectionStrings__Shop, Jwt__Key from secrets).

Because chiseled images have no curl, health probes are configured in the orchestrator (Kubernetes, Container Apps) against /health, not with a Dockerfile HEALTHCHECK.

Docker Compose runs the API and SQL Server together for local development and integration tests; use named volumes for the database and a health check on SQL Server before starting the API.

Syntax#

Shell Illustrative
docker build -t shopsmart-api:1.0 .
docker run -d -p 8080:8080 -e ConnectionStrings__Shop="…" shopsmart-api:1.0
curl http://localhost:8080/health

Code example: Dockerfile for the ShopSmart API#

DockerfileDockerfile Illustrative
# ── Build ──
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ShopSmart.Api/ShopSmart.Api.csproj ShopSmart.Api/
RUN dotnet restore ShopSmart.Api/ShopSmart.Api.csproj
COPY . .
RUN dotnet publish ShopSmart.Api/ShopSmart.Api.csproj -c Release -o /app/publish --no-restore /p:UseAppHost=false


# ── Runtime: chiseled = no shell, no package manager, non-root ──
FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled AS final
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_ENVIRONMENT=Production \
    ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "ShopSmart.Api.dll"]
Expected output
docker build produces an image of roughly 120 MB (the SDK stage is discarded). docker run -p 8080:8080 starts the API as a non-root user; curl http://localhost:8080/health returns “Healthy” and http://localhost:8080/api/products returns JSON.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Restoring with only the .csproj copied lets Docker reuse that layer until dependencies change.
  2. dotnet publish creates a framework-dependent build in /app/publish.
  3. The chiseled runtime image is small and already runs as a non-root user.
  4. ASPNETCORE_HTTP_PORTS=8080 matches EXPOSE; TLS is handled outside the container.
  5. All other settings (connection strings, JWT keys) arrive as environment variables or mounted secrets at runtime.

Real-world use case#

ShopSmart builds this image in GitHub Actions, scans it with Trivy, pushes it to Azure Container Registry and deploys it to Azure Container Apps with liveness and readiness probes on /health.

Common mistakes#

HEALTHCHECK with curl in a chiseled image

There is no curl or shell. Configure probes in the orchestrator, or use the non-chiseled image if you need tools.

Baking secrets into images

ENV Jwt__Key=… in a Dockerfile is readable by anyone who can pull the image. Inject secrets at runtime.

Running migrations from every container start

Several replicas racing to migrate the database cause failures. Run migrations as a separate deployment step.

Best practices#

  • Multi-stage builds with cached restore
  • Chiseled runtime images for APIs
  • Probes on /health in the orchestrator
  • Secrets and connection strings at runtime

Remember this#

sdk:9.0 builds → aspnet:9.0-noble-chiseled runs.

Port 8080, non-root, no shell.

Config via ConnectionStrings__X env vars.

Practice exercise#

Your task: Write a docker-compose.yml that starts SQL Server and the API, waits for SQL Server to be healthy, and maps the API to port 5080.

Need a hint?
depends_on: { db: { condition: service_healthy } } and a sqlcmd health check on the db service.
Show solution
YAML Illustrative
services:
  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: "Y"
      MSSQL_SA_PASSWORD: ${SA_PASSWORD}
    volumes:
      - sqldata:/var/opt/mssql
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P \"$MSSQL_SA_PASSWORD\" -Q 'SELECT 1' || exit 1"]
      interval: 10s
      retries: 10


  api:
    build: .
    ports:
      - "5080:8080"
    environment:
      ConnectionStrings__Shop: "Server=db;Database=ShopSmart;User Id=sa;Password=${SA_PASSWORD};TrustServerCertificate=True"
    depends_on:
      db:
        condition: service_healthy


volumes:
  sqldata:

Mini quiz#

Docker quizMultiple choice Question 1 of 3

What is special about chiseled .NET images?

Score so far: 0/0

Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.