Mert Özen Further With Every Line
Backend .NET Core Web API — #28

Containerizing with Docker: Putting the Application into a Package That Runs the Same Everywhere

Mert Özen Aug 23, 2026 13 min 21 views
Containerizing with Docker: Putting the Application into a Package That Runs the Same Everywhere

We cover containerizing the application with Docker: what a container is, how it solves the "it worked on my machine" problem, packaging a .NET app with a Dockerfile and multi-stage build.

Your API is ready: it works correctly, it's fast, secure, and reports its health. Now it's time to take it out of your own machine and open it to the world. But here a classic problem comes into play: something that works on your machine may not work on another. A different operating system, a missing library, a different .NET version... This is exactly what Docker exists to end. Today we talk about putting the application into a portable package that runs the same everywhere.

The "It Worked on My Machine" Problem

There's a story every developer runs into. Your code works flawlessly on your computer. You move it to the server and suddenly everything blows up. The reason is usually an environment difference: a setting, a version, a dependency that exists on your machine but not on the server. You grumble "but it worked on mine" for hours. This is one of the oldest and most frustrating problems in the software world.

The root of the problem is this: the application isn't enough on its own; it also needs the environment around it to run. When that environment changes from one machine to another, the application behaves differently too. So what if we could put the application, along with everything it needs, into a single package? So that wherever we go, when we open that package, everything inside is exactly the same? This is the idea of a container.

What Is a Container?

A container is putting your application and everything needed for it to run (the code, the runtime, libraries, settings) into a single self-sufficient package. This package, on whatever machine it runs, its contents don't change. However it behaves on your machine, it behaves exactly the same on the server. There's no such thing as an environment difference, because you've put the environment inside the package too.

Think of it like a shipping container; that's actually where the name comes from. Transporting goods by ship used to be complex; each cargo was a different size, a different shape. Standard containers changed everything: whatever you put inside, the outside of the container is always the same. The ship, the crane, the truck; they all know how to work with the container and don't care what's inside. A Docker container is this standard box for software: whatever's inside, it's always transported and run the same way from the outside.

Docker Image and Container

Separating the two terms is important. A Docker image is a frozen mold of the application; your application and all its needs stay inside, packaged. A container is a running instance of this image. Think of the relationship this way: the image is like a class, and the container is an object produced from that class. You can run as many containers as you want from one image; they all come from the same mold, they're all identical to each other.

This distinction matters for this reason: you create the image once, then move it wherever you want and run it there as a container. The image is unchanging and portable; the container is its live, running state. So the flow "create the image, move it everywhere, run it there" is the foundation of all deployment logic.

Dockerfile: The Packaging Recipe

You tell Docker how to package your application with a recipe file; this file is called a Dockerfile. It contains step-by-step instructions: which base environment to start from, where to copy the code, how to compile it, how to run it. For .NET applications, a typical Dockerfile looks like this:

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "KullaniciApi.dll"]

This file consists of two sections, and this is called a multi-stage build. The first section starts from an environment containing the .NET SDK; it copies the code and compiles the application. The second section starts from a much smaller environment (just enough to run the application, without build tools) and copies the output compiled in the first section here. The ENTRYPOINT at the end says which command will be run when the container starts.

Why Is Multi-Stage Build Important?

Understanding the logic of this two-stage structure is valuable. To compile the application you need a whole SDK; but to just run the application you don't need that much, a much smaller runtime is enough. A multi-stage build does the compilation in the large environment but puts only what's needed to run into the result image.

Think of making furniture. While producing the furniture you use a lot of tools, workbenches, raw materials; a whole workshop is needed. But when delivering to the customer, you don't send the workshop, only the finished furniture. A multi-stage build does exactly this: the build "workshop" stays in the first stage, and only the finished "furniture," that is, the running application, enters the final image. The result: a much smaller, faster-to-transport, and more secure image. A small image provides an advantage in both storage and transport, and since there's less inside it, the attack surface narrows.

Building and Running the Image

Once the Dockerfile is ready, you build the image with a single command. This command applies the recipe from start to finish and produces a ready-to-run image at the end:

docker build -t kullanici-api .

The -t kullanici-api here gives the image a name (tag); the dot at the end means "look for the recipe in this folder." After the image is built, you run it as a container:

docker run -p 8080:8080 kullanici-api

The -p 8080:8080 here connects the port inside the container to a port on your machine; so you can access the application from the outside. The moment you run this command, your application stands up inside the container. And here's the beauty of it: whether you run this container on your machine, on a server, or in a cloud environment, everything inside behaves exactly the same. The sentence "it worked on my machine" now loses its meaning.

A Small Experiment

First install Docker on your machine. Then add a Dockerfile like above to your project's root directory (adapt the file and project names to your own project). Build the image with the docker build command and watch how each step of the command is executed. Then run the container with docker run and access your application from the browser; see that it responds just like you ran it locally. If you like, go one step further: make a request to the /health endpoint you added in the twenty-seventh article through the container and observe that the health check works in the container environment too. This experiment turns deployment from an abstract concept into a concrete package you hold in your hands.

In the next article we'll automate this packaging job. We'll move on to CI/CD and talk about how, with GitHub Actions, the application is automatically tested, compiled, and made ready for deployment every time you update the code. The Docker steps we did by hand today will become part of an automated process there.