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.
On this page · 5 min · Advanced
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Advanced
- Updated
- Oct 29, 2026
Related tutorials
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#
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#
# ── 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 /app/publish .
ENV ASPNETCORE_ENVIRONMENT=Production \
ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "ShopSmart.Api.dll"]
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#
- Restoring with only the .csproj copied lets Docker reuse that layer until dependencies change.
- dotnet publish creates a framework-dependent build in /app/publish.
- The chiseled runtime image is small and already runs as a non-root user.
- ASPNETCORE_HTTP_PORTS=8080 matches EXPOSE; TLS is handled outside the container.
- 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#
There is no curl or shell. Configure probes in the orchestrator, or use the non-chiseled image if you need tools.
ENV Jwt__Key=… in a Dockerfile is readable by anyone who can pull the image. Inject secrets at runtime.
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?
Show solution
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#
What is special about chiseled .NET images?
Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions
My notes#
Saved in this browser.