
What Is Docker Compose?
Docker Compose is a tool for defining and running multi-container applications. Instead of manually creating and configuring each container, you can describe the application's services, networking, ports, volumes and other settings in a YAML file.
For a .NET application, this could mean having one container running the ASP.NET Core application and another running a database such as SQL Server or PostgreSQL. Docker Compose creates the necessary network between the containers, allowing them to communicate with one another.
This makes Compose particularly useful when an application consists of more than one component. Rather than having to remember a collection of Docker commands, the entire application environment can be started with a single command.
Preparing the .NET Application
For this example, we’ll assume that we have a simple ASP.NET Core application called MyDockerApp.
The application can be created using the .NET CLI with the following command:
dotnet new web -n MyDockerApp
Once the project has been created, it can be tested normally using:
dotnet run
Before introducing Docker Compose, it is useful to make sure the application works correctly on the development machine. Docker should be used to package and deploy an application that already works rather than introducing another layer of complexity while trying to diagnose application problems.
Creating the Dockerfile
The first step in containerising the application is to create a Dockerfile in the root of the project.
A multi-stage Docker build is a good approach for .NET applications because the .NET SDK is required to build the application but isn't normally required in the final production container.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["MyDockerApp.csproj", "."] RUN dotnet restore "MyDockerApp.csproj" COPY . . RUN dotnet publish "MyDockerApp.csproj" -c Release -o /app/publish --no-restore FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final WORKDIR /app COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "MyDockerApp.dll"]
The first stage uses the .NET SDK image to restore, build and publish the application. The second stage uses the smaller ASP.NET Core runtime image and copies only the published application into it.
This approach keeps the final image smaller and avoids shipping development and build tools that the application doesn't need at runtime. Microsoft's .NET container guidance recommends separating build and runtime concerns in this way.
Creating the Compose File
With the Dockerfile in place, we can create a compose.yaml file in the same directory as the project.
A simple Compose configuration might look like this:
services:
web:
build:
context: .
ports:
- "8080:8080"
The services section defines the individual components that make up the application.
In this example, the service is called web. The build section tells Docker Compose to build an image using the Dockerfile in the current directory.
The ports section maps port 8080 on the host machine to port 8080 inside the container.
Modern Docker Compose uses the Compose Specification, so a version property is no longer required in the Compose file. The older version 2.x and 3.x formats have been consolidated into the Compose Specification.
Building and Starting the Application
With the Dockerfile and compose.yaml in place, the application can be built and started using:
docker compose up --build
The --build option tells Docker Compose to rebuild the image before starting the containers.
Once the container has started, the application should be available at:
http://localhost:8080
The advantage here is that Docker Compose handles the creation and startup of the container for us. If the image has already been built and there have been no changes requiring a rebuild, the application can subsequently be started with:
docker compose up
Docker documents docker compose up as the standard way to create and start the services defined by a Compose file.
Running the Application in the Background
When deploying an application, you will often want the containers to continue running after closing the terminal.
Docker Compose provides the -d option for detached mode:
docker compose up -d
The command returns control to the terminal while the containers continue running in the background.
You can check the current state of the services with:
docker compose ps
This is useful for quickly checking whether the application container is running and which ports have been exposed.
Viewing Application Logs
When something goes wrong with a containerised application, logs are usually the first place to look.
Docker Compose makes it easy to view the logs for the application:
docker compose logs
You can also follow the logs as they are generated:
docker compose logs -f
For a larger application containing several services, you can request the logs for a particular service:
docker compose logs web
This is particularly useful when an application depends on other containers such as a database.
Adding a Database
One of the biggest advantages of Docker Compose becomes apparent when an application requires additional services.
For example, suppose our .NET application uses PostgreSQL. Instead of installing PostgreSQL directly onto the development or deployment machine, it can be defined as another Compose service.
The compose.yaml could be expanded to:
services:
web:
build:
context: .
ports:
- "8080:8080"
depends_on:
- db
db:
image: postgres:18
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: example
The application and database are now separate containers managed by Docker Compose.
The depends_on setting tells Compose that the web service depends on the database service. Compose also creates a network for the application, allowing services to communicate using their service names. This means the .NET application can use db as the database hostname rather than trying to locate the database using an IP address.
Persisting Database Data
Containers are designed to be disposable, so important data should not normally be stored only inside a container's writable filesystem.
Docker volumes can be used to persist database data independently of the database container.
For example:
services:
db:
image: postgres:18
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: example
volumes:
- db-data:/var/lib/postgresql
volumes:
db-data:
The named volume called db-data allows the database data to survive if the database container is removed and recreated.
This separation between containers and persistent data is an important part of designing reliable containerised applications.
Configuring the .NET Application
Configuration should generally be supplied to the container rather than being hard-coded into the application.
Docker Compose allows environment variables to be specified for a service.
For example:
services:
web:
build:
context: .
ports:
- "8080:8080"
environment:
ASPNETCORE_ENVIRONMENT: Production
ConnectionStrings__DefaultConnection: "Host=db;Database=myapp;Username=myapp;Password=example"
ASP.NET Core converts the double underscore in an environment variable name into a configuration hierarchy separator. This means ConnectionStrings__DefaultConnection corresponds to the ConnectionStrings:DefaultConnection configuration value used by the application.
For real deployments, passwords and other sensitive information should not be committed directly into the Compose file. Docker Compose supports secrets, which can be used to keep sensitive values separate from the main configuration.
Stopping the Application
When the application is no longer required, the containers can be stopped using:
docker compose down
This removes the containers and networks created by Compose but leaves named volumes in place by default.
If you also want to remove the volumes, you can use:
docker compose down -v
Care should be taken with this command when a database is involved because removing its volume can permanently remove the stored database data.
Deploying to a Server
The same Docker Compose configuration can be used as part of a deployment process on a suitable server.
The server needs Docker and Docker Compose support. Docker Desktop includes Docker Compose on Windows, macOS and Linux, while Docker Compose can also be installed as a plugin on Linux systems.
The application image can either be built directly on the server or, more commonly, built as part of a deployment pipeline and pushed to a container registry.
For a straightforward deployment where the source code is available on the server, the process might be as simple as:
docker compose build docker compose up -d
For larger projects, a CI/CD pipeline can build the Docker image, run tests, push the image to a registry and then deploy the new version to the target server.
HTTPS and Production Deployments
When deploying an ASP.NET Core application for real users, HTTPS should be configured appropriately.
There are several ways of handling HTTPS with Docker. One option is to terminate HTTPS at a reverse proxy such as Nginx or another web server and forward requests to the ASP.NET Core container.
Another option is to configure HTTPS directly within ASP.NET Core.
For local development, ASP.NET Core provides development certificates, and Microsoft documents how these can be mounted into a container when using Docker Compose. Production deployments should use a certificate issued by a trusted certificate authority.
For many production environments, terminating HTTPS at a reverse proxy is a cleaner approach because the reverse proxy can also handle certificates, HTTP-to-HTTPS redirection and other web-server responsibilities.
Keeping the Production Image Small
One of the important considerations when deploying .NET applications with Docker is the size of the final image.
The SDK image contains everything required to build a .NET application, including compilers and development tools. The ASP.NET Core runtime image contains the components required to run an ASP.NET Core application.
Using a multi-stage Dockerfile means that the final container doesn't need to contain the SDK or the application's source code.
This makes the resulting image smaller and reduces the amount of software that needs to be present in the production environment.
It also means that deploying a new version becomes a matter of replacing the container with a new one based on the updated image.
Using Docker Compose for More Than Deployment
Although Docker Compose is often associated with local development, it can be useful throughout the application lifecycle.
A Compose file can describe the services required for development, testing, staging and production, with different configuration files or profiles used where appropriate.
For example, a development environment might contain the .NET application, SQL Server and a mail-testing service. A production environment might instead use an external database and a reverse proxy.
The benefit is that the relationships between the application's services can be described as code rather than relying on manually configured machines.
Conclusion
Docker Compose provides a straightforward way to package and deploy a .NET application alongside the services it depends on.
For a simple ASP.NET Core application, a Dockerfile and a small compose.yaml file are enough to build and run the application in a consistent environment. As the application grows, Docker Compose can manage additional services such as databases, caches and supporting applications.
The real benefit is consistency. Instead of documenting a long list of installation and configuration steps, the application's environment can be described in code and started with a small number of Docker commands.
For .NET developers, combining multi-stage Docker builds with Docker Compose provides a practical foundation for moving applications from a development machine towards repeatable deployments.
Become a member
Get the latest news right in your inbox. It's free and you can unsubscribe at any time. We hate spam as much as we do, so we never spam!
Read next
Multi-Stage Docker Builds for .NET
Multi-stage Docker builds provide a cleaner and more efficient way to package .NET applications. By separating the build environment from the final runtime image, you can keep development tools out of production, reduce image size and create more focused containers. In this article, we’ll look at how multi-stage Docker builds work and how to use them with modern .NET applications.
Running SQL Server in Docker for Local Dev
