Dockerizing the .NET Core API, Angular and MS SQL Server
Introduction
Containerizing applications provides a consistent runtime environment, simplifies deployment, and improves scalability. In this article, we’ll walk through the process of “dockerizing” a Contact Management Application that follows clean architecture principles. The application comprises a .NET Core API, an Angular frontend, and an MS SQL Server database. By the end of this guide, you’ll have a reproducible, containerized environment ready to run locally or in production.
This guide will cover:
- Why containerization is essential.
- Setting up a Dockerfile for the .NET Core API.
- Configuring a docker-compose.yml file to run both the API and MS SQL Server.
- Setting up database initialization and migration.
- Running the application locally using Docker.
Part of the Series
This article is part of a series on building and understanding the Contact Management Application, a sample project showcasing clean architecture principles. Below are the other articles in the series:
- Clean Architecture: Introduction to the Project Structure
- Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
- Clean Architecture: Validating Inputs with FluentValidation
- Clean Architecture: Dependency Injection Setup Across Layers
- Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC)
- Clean Architecture: Implementing Activity Logging with Custom Attributes
- Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions
- Clean Architecture: Using Dapper for Data Access and Repository Pattern
- Clean Architecture: Best Practices for Creating and Using DTOs in the API
- Clean Architecture: Error Handling and Exception Management in the API
- Clean Architecture: Dockerizing the .NET Core API, Angular and MS SQL Server (You are here)
- Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts
1. Why Dockerize the Application?
By dockerizing the Contact Management Application, we ensure that it runs the same way on different machines, regardless of the underlying host system. The main benefits include:
- Consistency: Containers ensure that the application and its dependencies run consistently across development, staging, and production environments.
- Simplified Deployment: Docker makes it easy to package the application and its dependencies into a single image, allowing it to be deployed across multiple environments with ease.
- Isolation: Each container runs in isolation, ensuring that dependencies do not conflict with other applications running on the same host.
- Scalability: Containers can easily be scaled horizontally, making it simple to add more instances as demand grows.
2. Setting Up the Dockerfile
2.1 .NET Core API
To containerize the .NET Core API, create a Dockerfile
that outlines how to build and run the service. A multi-stage build approach ensures smaller, more efficient runtime images.
Here is the Dockerfile for the Contact Management Application:
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8000
ENV ASPNETCORE_URLS=http://+:8000
RUN groupadd -g 2000 dotnet \
&& useradd -m -u 2000 -g 2000 dotnet
USER dotnet
# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
ARG DOTNET_SKIP_POLICY_LOADING=true
WORKDIR /src
COPY ["Contact.Api/Contact.Api.csproj", "Contact.Api/"]
COPY ["Contact.Application/Contact.Application.csproj", "Contact.Application/"]
COPY ["Contact.Domain/Contact.Domain.csproj", "Contact.Domain/"]
COPY ["Contact.Infrastructure/Contact.Infrastructure.csproj", "Contact.Infrastructure/"]
COPY ["Contact.Common/Contact.Common.csproj", "Contact.Common/"]
RUN dotnet restore "./Contact.Api/Contact.Api.csproj"
COPY . .
WORKDIR "/src/Contact.Api"
RUN dotnet build "./Contact.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
RUN ls /app/build
# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Contact.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final
WORKDIR /app
# COPY --from=publish /app/publish/Contact.Api.dll .
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Contact.Api.dll"]
Explanation:
- Build Stage: Uses the .NET SDK image to restore dependencies and build the application.
- Publish Stage: Publishes a Release build to a dedicated folder.
- Final Stage: Leverages a lightweight .NET runtime image to run the published output.
2.2 Angular (Frontend)
# Create image based off of the official Node 10 image
FROM node:22-alpine as builder
# Copy dependency definitions
COPY package*.json ./
## installing and Storing node modules on a separate layer will prevent unnecessary npm installs at each build
## --legacy-peer-deps as ngx-bootstrap still depends on Angular 14
RUN npm install --legacy-peer-deps && mkdir /app && mv ./node_modules /app/node_modules
# Change directory so that our commands run inside this new directory
WORKDIR /app
# Get all the code needed to run the app
COPY . .
# Build server side bundles
RUN npm run build
# Stage 2: Serve app with nginx server
# Use official nginx image as the base image
FROM nginx:alpine
# Remove default Nginx website
RUN rm -rf /usr/share/nginx/html/*
# Copy the build output to replace the default nginx contents.
COPY --from=builder /app/dist/contacts/browser /usr/share/nginx/html
# Copy the nginx file to fix fallback issue on refreshing.
COPY /nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 8080
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]
Explanation:
- Stage 1: Build the Application
- We start by using the official Node image to build the application.
- The
npm install
command restores the project dependencies, followed bynpm
run build which compiles the project in Release mode.
- Stage 2: Use the Runtime Image
- After building the project, we use a lightweight nginx:alpine image to run the compiled application.
- The CMD directive runs the application
3. Configuring Docker Compose for the API, Frontend, Loadbalancer (Nginx) and MS SQL Server
To orchestrate both the .NET Core API, Angular, and MS SQL Server, we use Docker Compose, which allows us to define and run multiple services as a single unit.
Create a docker-compose.yml file in the root of your project directory:
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
# ports:
# - 8080:8080
depends_on:
- api
networks:
- mssql_network
api:
build:
context: ./backend/src
dockerfile: Dockerfile
args:
- configuration=Release
# ports:
# - 8000:8000
environment:
- ASPNETCORE__ENVIRONMENT=${ENVIRONMENT}
- AppSettings__ConnectionStrings__DefaultConnection=Server=${SQL_SERVER};Database=${SQL_DATABASE};User ID=${SQL_USER};Password=${SQL_PASSWORD};Trusted_Connection=False;Encrypt=False;
- AppSettings__Secret=${JWT_SECRET}
- AppSettings__Issuer=${JWT_ISSUER}
- AppSettings__Audience=${JWT_AUDIENCE}
- AppSettings__PasswordResetUrl=${PASSWORD_RESET_URL}
- SmtpSettings__SmtpServer=${SMTP_SERVER}
- SmtpSettings__Port=${SMTP_PORT}
- SmtpSettings__Username=${SMTP_USERNAME}
- SmtpSettings__Password=${SMTP_PASSWORD}
- SmtpSettings__FromEmail=${SMTP_FROM_EMAIL}
- SmtpSettings__EnableSsl=${SMTP_ENABLE_SSL}
depends_on:
- mssql
networks:
- mssql_network
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: sqlserver_express
environment:
- ACCEPT_EULA=Y
- MSSQL_PID=Express # Specifies the edition to run as Express
- MSSQL_SA_PASSWORD=${SQL_PASSWORD} # Set the SA (System Administrator) password
ports:
- "1433:1433" # Expose SQL Server port 1433
volumes:
- mssql_data:/var/opt/mssql # Persist database data outside of the container
- ./backend/scripts:/scripts # Mount for SQL scripts
entrypoint:
- /bin/bash
- -c
- |
/opt/mssql/bin/sqlservr & sleep 15s;
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${SQL_PASSWORD} -d master -i /scripts/seed-data.sql; wait
networks:
- mssql_network
nginx: #name of the fourth service
build: loadbalancer # specify the directory of the Dockerfile
container_name: nginx
restart: always
ports:
- "80:80" #specify ports forewarding
depends_on:
- frontend
- api
networks:
- mssql_network
volumes:
mssql_data: # Named volume to persist data
networks:
mssql_network:
driver: bridge
Explanation:
- Frontend Service
- The frontend service uses the Dockerfile to build the Angular project
- The API listens on port 8000, mapped to the container’s internal port 8000.
- The depends_on ensures that the frontend service waits for the api service and db service (MS SQL Server) to be ready before starting.
- API Service
- The api service uses the Dockerfile to build the .NET Core API.
- The API listens on port 8080, mapped to the container’s internal port 8080.
- It sets up the database connection string using environment variables.
- The depends_on ensures that the API service waits for the db service (MS SQL Server) to be ready before starting.
- DB Service (MS SQL Server)
- The db service pulls the official MS SQL Server Docker image and configures it with an environment variable for the password (
SA_PASSWORD
). - The database listens on port 1433.
- The db service pulls the official MS SQL Server Docker image and configures it with an environment variable for the password (
- Network
- Both services are connected via a Docker network (contact-management-network), ensuring they can communicate internally.
4. Database Initialization
In a real-world application, you often need to initialize the database and apply migrations when starting the application. This can be achieved by running sql script using entrypoint when MS sql container is up. All env variables will be read from .env file which is on root of the project.
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: sqlserver_express
environment:
- ACCEPT_EULA=Y
- MSSQL_PID=Express # Specifies the edition to run as Express
- MSSQL_SA_PASSWORD=${SQL_PASSWORD} # Set the SA (System Administrator) password
ports:
- "1433:1433" # Expose SQL Server port 1433
volumes:
- mssql_data:/var/opt/mssql # Persist database data outside of the container
- ./backend/scripts:/scripts # Mount for SQL scripts
entrypoint:
- /bin/bash
- -c
- |
/opt/mssql/bin/sqlservr & sleep 15s;
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${SQL_PASSWORD} -d master -i /scripts/seed-data.sql; wait
networks:
- mssql_network
Now, whenever you build and run the Docker containers, the database will be automatically initialized with the latest schema.
5. Running the Application Locally
To run the Contact Management Application locally with Docker, follow these steps:AA
docker-compose up --build
This command will:
- Build the .NET Core API image.
- Pull the MS SQL Server image.
- Start both containers, connecting them via the Docker network.
- Access the Application:
- Once the containers are running, you can access the API at
http://localhost:5000
.
- Once the containers are running, you can access the API at
- Verify the Database:
- The MS SQL Server will be accessible on port 1433. You can connect to the database using SQL Server Management Studio (SSMS) or any other SQL client using the credentials provided in the docker-compose.yml file.
6. Docker Compose for Development and Production
Docker Compose can be tailored for different environments (development, staging, production) by using overrides. For instance, you can create a `docker-compose.yml
` for production with different configurations, such as nginx load balancer, so frontend and api both are exposed using same domain. `docker-compose.debug.yml
` using hot reloading for the API and Frontend and used for the development
Example docker-compose.debug.yml
services:
frontend:
build:
context: ./frontend
dockerfile: debug.dockerfile
command: ["npm", "run", "start:debug"]
ports:
- 4200:4200
- 49153:49153
volumes:
- ./frontend:/app
- /app/node_modules
stdin_open: true
tty: true
depends_on:
- api
networks:
- mssql_network
api:
build:
context: ./backend/src
dockerfile: Debug.Dockerfile
command: ["dotnet", "watch", "--project", "Contact.Api/Contact.Api.csproj", "run", "--urls", "http://0.0.0.0:5000"]
ports:
- 5000:5000
environment:
- ASPNETCORE__ENVIRONMENT=${ENVIRONMENT}
- DOTNET_SKIP_POLICY_LOADING=false
- AppSettings__ConnectionStrings__DefaultConnection=Server=${SQL_SERVER};Database=${SQL_DATABASE};User ID=${SQL_USER};Password=${SQL_PASSWORD};Trusted_Connection=False;Encrypt=False;
- AppSettings__Secret=${JWT_SECRET}
- AppSettings__Issuer=${JWT_ISSUER}
- AppSettings__Audience=${JWT_AUDIENCE}
- AppSettings__PasswordResetUrl=${PASSWORD_RESET_URL}
- SmtpSettings__SmtpServer=${SMTP_SERVER}
- SmtpSettings__Port=${SMTP_PORT}
- SmtpSettings__Username=${SMTP_USERNAME}
- SmtpSettings__Password=${SMTP_PASSWORD}
- SmtpSettings__FromEmail=${SMTP_FROM_EMAIL}
- SmtpSettings__EnableSsl=${SMTP_ENABLE_SSL}
volumes:
- ./backend/src:/app
- ~/.vsdbg:/remote_debugger:rw
depends_on:
- mssql
networks:
- mssql_network
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: sqlserver_express
environment:
- ACCEPT_EULA=Y
- MSSQL_PID=Express # Specifies the edition to run as Express
- MSSQL_SA_PASSWORD=${SQL_PASSWORD} # Set the SA (System Administrator) password
ports:
- "1433:1433" # Expose SQL Server port 1433
volumes:
- mssql_data:/var/opt/mssql # Persist database data outside of the container
- ./backend/scripts:/scripts # Mount for SQL scripts
entrypoint:
- /bin/bash
- -c
- |
/opt/mssql/bin/sqlservr & sleep 15s;
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${SQL_PASSWORD} -d master -i /scripts/seed-data.sql; wait
networks:
- mssql_network
volumes:
mssql_data: # Named volume to persist data
networks:
mssql_network:
driver: bridge
This setup mounts the local directories into the container, allowing you to modify code without rebuilding the container.
Conclusion
Dockerizing the Contact Management Application makes it easier to deploy and run consistently across different environments. By leveraging Docker Compose, we can orchestrate the .NET Core API and MS SQL Server in a local development setup, and this setup can be extended to staging or production environments with minimal changes.
In the next article, we will cover how to seed initial data using Docker Compose and SQL scripts.
For more details, check out the full project on GitHub:
GitHub Repository: Contact Management Application
Get Involved!
- Try the Code: Test the examples provided and share your results in the comments.
- Follow Us: Stay updated by following us on GitHub.
- Subscribe: Sign up for our newsletter to receive expert Azure development tips.
- Join the Conversation: What challenges have you faced while running .Net or Angular app in Docker? Share your experiences in the comments below!
Discover more from Nitin Singh
Subscribe to get the latest posts sent to your email.
5 Responses
[…] Clean Architecture: Dockerizing the .NET Core API and MS SQL Server […]
[…] Clean Architecture: Dockerizing the .NET Core API and MS SQL Server […]
[…] Clean Architecture: Dockerizing the .NET Core API and MS SQL Server […]
[…] Clean Architecture: Dockerizing the .NET Core API and MS SQL Server […]
[…] Clean Architecture: Dockerizing the .NET Core API and MS SQL Server […]