Our Summer Sale is Live! 🎉
Everything 30% off with code SUMMER30! (Excl. Team and VS Pro)
00
Days
00
Hrs
00
Min
00
Sec
Get 30% off anything!

Introduction to Docker for .NET Developers

15/08/2025

Containers package your app with everything it needs and run it the same way everywhere. No fiddling with machine-specific setups, no “works on my machine” jokes.

This guide keeps things hands-on: images vs containers, writing a Dockerfile,docker run basics, and a small docker-compose.yml to run your API plus a database.


Images vs containers (quick mental model)

  • Image → a read-only snapshot of your app and its dependencies (like a recipe).
  • Container → a running instance of that image (like a prepared dish). You can start/stop/remove containers without changing the image.

Build an image once, run containers many times.


A tiny .NET Minimal API to containerize

Create a folder HelloApi and drop this Program.cs inside:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "API is up");
app.MapGet("/hello/{name}", (string name) => new { message = $"Hello, {name}" });

app.Run();

.dockerignore (do this first)

Prevent large and irrelevant files from bloating your image:

bin/
obj/
*.user
*.suo
.vscode/
.idea/
**/*.Secrets.json

Dockerfile (multi-stage build for .NET 9)

Create Dockerfile next to your .csproj:

# 1) Build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src

# copy csproj first to take advantage of layer caching
COPY HelloApi.csproj .
RUN dotnet restore

# copy the rest and publish
COPY . .
RUN dotnet publish -c Release -o /app /p:UseAppHost=false

# 2) Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080

COPY --from=build /app ./
ENTRYPOINT ["dotnet", "HelloApi.dll"]

Why this layout?

  • Multi-stage keeps the final image small (SDK isn’t in the runtime layer).
  • Copying the .csproj before source allows restore caching until dependencies change.

Build and run

# build the image (note the trailing dot)
docker build -t helloapi:dev .

# run a container mapping port 8080 in the container to 8080 on your machine
docker run --rm -p 8080:8080 helloapi:dev

Browse to http://localhost:8080/hello/Dotnet and you should see JSON.

Handy flags:

  • --rm removes the container when it exits.
  • -e KEY=VALUE sets environment variables (useful for connection strings, etc.).

Common Dockerfile instructions (short list)

  • FROM — base image (e.g., aspnet:9.0, sdk:9.0).
  • WORKDIR — sets the working folder for subsequent commands.
  • COPY — copies files into the image.
  • RUN — executes a command at build time (e.g., dotnet publish).
  • ENV — defines environment variables.
  • EXPOSE — documents the port your app listens on.
  • ENTRYPOINT / CMD — what runs when the container starts.

Docker Compose: API + Postgres locally

Create docker-compose.yml at the solution root to run multiple containers together:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    image: helloapi:dev
    ports:
      - "8080:8080"
    environment:
      ASPNETCORE_URLS: http://+:8080
      ConnectionStrings__Default: Host=db;Port=5432;Database=hello;Username=postgres;Password=postgres
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: hello
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  pgdata:

Run everything:

docker compose up --build
  • depends_on starts the database before the API (you should still add retry logic in your app).
  • The pgdata volume keeps your database files between restarts.

Dev workflow tips

  • Rebuild fast: keep your Dockerfile steps cache-friendly (copy .csproj, restore, then copy source).
  • Hot reload (local only): run dotnet watch on your host, and use a bind mount to share files into the container if you need the app to run inside Docker during dev:
    services:
      api:
        volumes:
          - ./:/src
        command: ["dotnet", "watch", "--project", "/src/HelloApi.csproj", "run", "--urls", "http://0.0.0.0:8080"]
    
  • Prune old stuff: docker system prune -f removes stopped containers and dangling images. Careful when using this as this will delete images and containers.

Quick troubleshooting

  • Port already in use
    Change the host mapping: -p 9090:8080 and browse http://localhost:9090.

  • File not found at runtime
    Ensure you COPY files into the runtime stage (or from the build stage). Check paths.

  • Slow builds
    Make sure .dockerignore is in place and you copy the .csproj separately to benefit from restore caching.

  • “Works outside Docker, fails inside”
    Check environment variables. Containers are isolated; they do not see your host’s env unless you pass them in (-e or environment: in Compose).


A tiny checklist

  • [ ] Use a multi-stage Dockerfile for .NET.
  • [ ] Keep a tidy .dockerignore.
  • [ ] Treat images as immutable—rebuild for changes.
  • [ ] Map ports explicitly and pass env vars consciously.
  • [ ] Use Compose when you have more than one container.
  • [ ] Persist data with volumes in Compose.

More!

# one-liners you’ll remember
docker build -t app:dev .
docker run --rm -p 8080:8080 app:dev
docker compose up --build
docker ps -a
docker logs <container>
docker exec -it <container> sh

About the Author

author_img

Nick Chapsas

Nick Chapsas is a .NET & C# content creator, educator and a Microsoft MVP for Developer Technologies with years of experience in Software Engineering and Engineering Management.

He has worked for some of the biggest companies in the world, building systems that served millions of users and tens of thousands of requests per second.

Nick creates free content on YouTube and is the host of the Keep Coding Podcast.

More courses by Nick Chapsas