If you've been building applications with C#, you've probably experienced this at least once.
Your API runs perfectly on your laptop. Your database connection works. Your background worker processes messages. Everything looks good.
Then you deploy it to another environment.
Suddenly, something breaks.
Maybe the server has a different runtime. Maybe an environment variable is missing. Maybe a dependency behaves differently. Or maybe the application expects a service that isn't running.
And now you're debugging the environment instead of your code.
This is one of the problems Docker helps solve.
Think of Your Application as a Delivery Package
When you publish a .NET application, you usually focus on the application itself.
You compile the code, produce the build output, configure the server, install the required runtime when necessary, set environment variables, and make sure external services are available.
That's a lot of moving parts.
Docker encourages you to describe the application's runtime environment alongside the application.
Instead of handing someone just your application and a list of setup instructions, you build an image that packages the application with its required runtime components and dependencies.
The goal is simple: make the application easier to run consistently across different environments.
Let's Make It Practical
Suppose you have an ASP.NET Core API.
On your machine, you run:
dotnet run
Everything works.
Now you want another developer to run the same API without manually reproducing your setup.
You create a Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish MyApi.csproj \
-c Release \
-o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]
Build the image:
docker build -t my-api .
Run it:
docker run --rm -p 8080:8080 my-api
Assuming the application is configured to listen on port 8080, your API is now accessible through the mapped host port.
You've described how to build the application package and how to start it. Someone else with a compatible Docker environment can use the same image rather than manually installing your application's runtime dependencies.
For a real project, you'd also configure environment-specific settings, secrets, networking, and persistent storage where needed.
But Here's What Docker Doesn't Magically Fix
This is where I think developers sometimes misunderstand containerisation.
Docker doesn't automatically make bad code good.
It won't fix a database query that takes ten seconds. It won't prevent duplicate payment processing. It won't make a failing external API reliable. And it won't secure an application simply because it runs inside a container.
If your application has a race condition, you can still have that race condition inside Docker.
If your worker processes the same transaction twice, containerising it won't solve the underlying idempotency problem.
Docker gives you a more consistent way to package and run software. Application correctness, resilience, and security still require deliberate engineering.
That distinction matters, especially when you're building APIs, payment services, or background workers.
Where Docker Gets Really Useful
The real benefit becomes more obvious when your application grows beyond a single API.
Imagine a .NET payment system containing:
- An ASP.NET Core API that receives requests.
- A background worker that processes queued transactions.
- A PostgreSQL database that stores records.
- A message broker that distributes events.
You can use Docker Compose to define the services and how they connect.
Your local environment becomes something you can describe in a file, share with your team, and recreate when needed.
That doesn't mean every production system should run exactly like your local Compose environment. Production still needs proper secrets management, monitoring, backups, access controls, and deployment practices.
But having a reproducible starting point makes development and troubleshooting much easier.
My Biggest Takeaway
I used to think of deployment as the final step after writing the code.
Now, I think of the runtime environment as part of the application engineering process.
The code matters, but so does the environment in which that code executes.
Docker helps bring those two concerns closer together.
And perhaps that's the most useful way to approach learning it: don't start by memorising commands. Start with an application you already understand, package it, run it in isolation, and see what changes when you move it between environments.
You'll quickly discover which parts of your application were truly portable and which parts were relying on your machine without you realising it.
Question for fellow .NET developers:
What's the most frustrating “works on my machine” bug you've encountered, and did Docker solve it—or just reveal a different problem?
Top comments (0)