🔥 Pragmatic .NET Code Rules Course is on Presale - 40% off!BUY NOW

How to Run .NET 10 with Docker and Traefik (Real-World Setup)

Jan 26 2026

This issue is self-sponsored. By supporting my work and purchasing my products, you directly help me keep this newsletter free and continue creating high-quality, practical .NET content for the community. Thank you for the support 🙌 I’m currently building a new course, Pragmatic .NET Code Rules, focused on creating a predictable, consistent, and self-maintaining .NET codebase using .editorconfig, analyzers, Visual Studio code cleanup, and CI enforcement. The course is available for pre-sale until the official release, with early-bird pricing for early adopters. You can find all the details here.

.NET 10 with Docker and Traefik – A Production-Ready Reverse Proxy Setup

Every .NET developer eventually runs into the same problem.

You start with one API → everything is fine. Then you add another service. Then a UI. Then a webhook endpoint. Then a background worker.

And suddenly your local environment looks like this: • localhost:5001localhost:5173localhost:6012 • “Wait… which service was on which port?” Ports collide. CORS starts breaking. Auth redirects fail. README files become outdated the moment you add a new service.

The problem is not .NET. The problem is how traffic is routed.

That’s exactly where Traefik fits in.


In this newsletter, we’ll build a complete .NET 10 + Docker + Traefik setup, including: • Two real .NET APIs • Correct reverse-proxy handling • Clean local URLs • A setup you can extend without rewriting everything

What we’re building

A simple but realistic setup: • Traefik as a reverse proxy (Docker provider) • Two .NET 10 Minimal APIs 1) Catalog API 2) Billing API

• Clean, human-readable URLs: 1) http://catalog.localhost 2) http://billing.localhost • Correct reverse-proxy handling in ASP. NET • Everything runs with one docker compose up This is not a toy example. This is a pattern you can keep using.

Project structure

Project Structure

Traefik is the only container exposed to the host. All APIs are internal and reachable only through Traefik.

Traefik & Docker Compose

docker-compose.yml:

C#
services: traefik: image: traefik:v3.6 container_name: traefik command: # Enable Docker provider - --providers.docker=true - --providers.docker.exposedbydefault=false  # Entry point (HTTP) - --entrypoints.web.address=:80  # Dashboard (local dev only) - --api.dashboard=true  # Useful while learning - --log.level=INFO - --accesslog=true ports: - "8080:80" volumes: # Traefik reads container labels from Docker - /var/run/docker.sock:/var/run/docker.sock:ro labels: - traefik.enable=true - traefik.http.routers.traefik.rule=Host(`traefik.localhost`) - traefik.http.routers.traefik.entrypoints=web - traefik.http.routers.traefik.service=api@internal  catalog-api: build: ./CatalogApi container_name: catalog-api environment: - ASPNETCORE_URLS=http://0.0.0.0:8080 labels: - traefik.enable=true - traefik.http.routers.catalog.rule=Host(`catalog.localhost`) - traefik.http.routers.catalog.entrypoints=web - traefik.http.services.catalog.loadbalancer.server.port=8080  billing-api: build: ./BillingApi container_name: billing-api environment: - ASPNETCORE_URLS=http://0.0.0.0:8080 labels: - traefik.enable=true - traefik.http.routers.billing.rule=Host(`billing.localhost`) - traefik.http.routers.billing.entrypoints=web - traefik.http.services.billing.loadbalancer.server.port=8080

Why does this work? • Traefik watches Docker and reads container labels • Services declare how they are exposed • No ports are exposed per service • Routing is declarative and local to the service

Catalog API (.NET 10)

This API represents a typical read-only service. Program.cs

C#
using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); // When running behind Traefik (reverse proxy), your app receives requests// with forwarded headers (X-Forwarded-For, X-Forwarded-Proto).// This middleware makes ASP.NET Core understand the original scheme/host.builder.Services.Configure<ForwardedHeadersOptions>(options =>{ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;  // For local Docker networks, you often need to clear these defaults. // In production, you should lock it down using KnownNetworks/KnownProxies. options.KnownIPNetworks.Clear(); options.KnownProxies.Clear();}); var app = builder.Build(); app.UseForwardedHeaders(); app.MapGet("/", (HttpContext ctx) =>{ // This response is intentionally "debuggy" so you can see what Traefik changes. return Results.Ok(new { service = "Catalog API", host = ctx.Request.Host.Value, scheme = ctx.Request.Scheme, path = ctx.Request.Path.Value, remoteIp = ctx.Connection.RemoteIpAddress?.ToString() });}); app.MapGet("/health", () => Results.Ok(new { status = "ok" })); // A realistic endpoint exampleapp.MapGet("/api/products", () =>{ var products = new[] { new { id = 1, name = "Keyboard", price = 89.99 }, new { id = 2, name = "Mouse", price = 39.99 } };  return Results.Ok(products);}); if (app.Environment.IsDevelopment()){ app.MapOpenApi();} app.Run();

Why forwarded headers matter

Without UseForwardedHeaders(): • ASP .NET thinks every request is http • Redirects break • Auth callbacks break • Absolute URLs are wrong This is mandatory behind any reverse proxy.

Dockerfile

YAML
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS buildWORKDIR /srcCOPY . .RUN dotnet publish -c Release -o /app FROM mcr.microsoft.com/dotnet/aspnet:10.0WORKDIR /appCOPY --from=build /app .EXPOSE 8080ENTRYPOINT ["dotnet", "CatalogApi.dll"]

Billing API (.NET 10)

This API represents a write-heavy, business-oriented service. Program.cs

C#
using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); builder.Services.Configure<ForwardedHeadersOptions>(options =>{ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;  options.KnownIPNetworks.Clear(); options.KnownProxies.Clear();}); builder.Services.AddOpenApi(); var app = builder.Build(); app.UseForwardedHeaders(); app.MapPost("/api/invoices/calculate", (InvoiceRequest request) =>{ var subtotal = request.Items.Sum(i => i.UnitPrice * i.Quantity); var tax = Math.Round(subtotal * request.TaxRate, 2);  return Results.Ok(new { subtotal, tax, total = subtotal + tax });}); app.MapGet("/health", () => Results.Ok("OK")); if (app.Environment.IsDevelopment()){ app.MapOpenApi();} app.Run(); public record InvoiceRequest( decimal TaxRate, List<InvoiceItem> Items); public record InvoiceItem( string Name, decimal UnitPrice, int Quantity);

Dockerfile

C#
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS buildWORKDIR /srcCOPY . .RUN dotnet publish -c Release -o /app FROM mcr.microsoft.com/dotnet/aspnet:10.0WORKDIR /appCOPY --from=build /app .EXPOSE 8080ENTRYPOINT ["dotnet", "BillingApi.dll"]

Running everything:

C#
docker compose up --build

Open: • http://catalog.localhost:8080 • http://catalog.localhost:8080/api/products • http://billing.localhost:8080/api/invoices/calculate • http://traefik.localhost:8080

Why this setup is actually good

✅ Clean URLs • No ports per service. ✅ Scales naturally • Adding a new service = copy labels. ✅ Matches production architecture • Reverse proxy first, services internal. ✅ No proxy config hell • Routing lives with the service.

How to extend this setup

HTTPS - Add a websecure entrypoint + cert resolver.

Middlewares - Rate limiting, headers, auth - reusable via labels.

Observability - Metrics, access logs, tracing - without touching .NET code.

Path-based routing - Switch from subdomains to /api/* if needed.

Wrapping up

Traefik is more than just a reverse proxy - it’s what makes Docker + .NET feel clean and predictable.

Instead of fighting ports and proxy configs, you get: • clear routing rules • production-like local environments • services that stay focused on business logic Once this setup is in place, adding new services becomes trivial, local development becomes boring (in a good way), and your architecture starts scaling naturally.

If you’re already running .NET in Docker, Traefik is the missing piece that makes everything click.

That's all for today!

Happy shipping 🚢

About the Author

Stefan Djokic is a Microsoft MVP and senior .NET engineer with extensive experience designing enterprise-grade systems and teaching architectural best practices.

There are 3 ways I can help you:

1. Pragmatic .NET Code Rules Course

Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.

Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.

2. Design Patterns Ebooks

Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.

Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.

3. Join 20,000+ subscribers

Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.

Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Architecture & Best Practices.

Subscribe to
TheCodeMan.net

Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.