Dockerizing for nginx
A multi-stage Dockerfile that builds the .NET API into a small runtime image, plus an nginx config that reverse-proxies to it — the same "build once, serve the output" shape as this docs site's own npm run build → nginx flow, just for an API instead of static files.
Multi-stage Dockerfile
Two stages: the build stage has the full SDK and compiles the app; the final stage only has the lightweight ASP.NET runtime and the already-compiled output. This keeps the shipped image small — no compiler, no source, no NuGet cache sitting in production.
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
# copy csproj files first so `dotnet restore` is cached
# separately from source changes
COPY ["YourApp.API/YourApp.API.csproj", "YourApp.API/"]
COPY ["YourApp.Application/YourApp.Application.csproj", "YourApp.Application/"]
COPY ["YourApp.Domain/YourApp.Domain.csproj", "YourApp.Domain/"]
COPY ["YourApp.Infrastructure/YourApp.Infrastructure.csproj", "YourApp.Infrastructure/"]
RUN dotnet restore "YourApp.API/YourApp.API.csproj"
COPY . .
WORKDIR /src/YourApp.API
RUN dotnet publish -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "YourApp.API.dll"]
docker-compose: API + database + nginx
nginx sits in front of the API container so the API itself is never exposed directly — nginx handles TLS termination, and the same nginx also happens to be exactly where the static docs site from earlier in this project would be served from.
services:
api:
build:
context: .
dockerfile: YourApp.API/Dockerfile
environment:
- ConnectionStrings__DefaultConnection=Server=db;Database=YourApp;User=sa;Password=${DB_PASSWORD};TrustServerCertificate=True
- JWT__Key=${JWT_KEY}
- JWT__Issuer=YourApp
depends_on:
- db
restart: unless-stopped
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=${DB_PASSWORD}
volumes:
- db-data:/var/opt/mssql
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- /var/www/docs:/var/www/docs:ro
depends_on:
- api
restart: unless-stopped
volumes:
db-data:
Secrets (DB_PASSWORD, JWT_KEY) come from a .env file next to docker-compose.yml — never committed, never baked into the image.
nginx: reverse proxy + static docs, side by side
server {
listen 80;
server_name yourapp.example.com;
# API — proxied to the container on the compose network
location /api/ {
proxy_pass http://api:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Docs site — plain static files, served through a "current" symlink
# rather than a fixed path, so a new release goes live by re-pointing
# one symlink instead of touching nginx at all (see Versioned Releases
# & Auto-Deploy).
location /docs/ {
alias /var/www/docs/current/;
try_files $uri $uri.html $uri/ =404;
}
}
No nginx -s reload is needed when a new release ships: nginx resolves /var/www/docs/current/ on every request, so it always follows wherever that symlink currently points — see Versioned Releases & Auto-Deploy for how the symlink gets flipped.
Building and running
docker compose up -d --build
docker compose logs -f api
TheCOPY *.csprojstep beforeCOPY . .is worth keeping even though it looks redundant: Docker caches each layer, so as long as no.csprojchanged,dotnet restoreis skipped entirely on the next build — only source changes trigger a re-restore.