
.net + angular - from source to container
Let's, as always, define a goal: What is required to achieve?
- Implement, deploy, and run SPA SSR and API in a container in Azure.
To reach the goal, we need to go through several steps:
- Implement the application.
- Build a container.
- Create an Azure Container Registry.
- Push the application to GitHub.
- Configure GitHub workflow to build and test.
- Configure and start app.
Implementation:
Here, we have two general possibilities:
- Use a template where .NET API and Angular are configured together and provided as a single solution.
- Build two different applications.
Creating two applications that are already preconfigured to work together may seem like an attractive option. However, it is important to remember that this approach also introduces a large number of "hidden" configurations that do not always behave the way a developer expects.
By creating two independent applications from the beginning, you retain full control over their configuration and setup. This also significantly reduces the coupling between the applications, making it possible to manage, develop, and deploy each one more independently. For these reasons, I believe the second approach is the better choice.
The implementation details of the application itself are beyond the scope of this article and may be covered in future posts. Here, we will focus on configuring the environment and organizing the deployment process.
Build a container
At this point, the application consists of two separate parts. From a deployment perspective, however, it is much more convenient to package both of them into a single container.
To achieve this, we can start with the standard .NET SDK image and extend it by installing Node.js.
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Install Node.js
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
Next, copy the source code of both applications into the container.
WORKDIR /src
#
# Copy projects
#
COPY api/ ./api/
COPY ui/ ./ui/
After that, build each application separately.
# ---------- Angular ----------
WORKDIR /src/ui
RUN npm ci --legacy-peer-deps
RUN npm run build
RUN ls -la /src/ui/dist
RUN find /src/ui/dist -maxdepth 3 -type f | head -20
# ---------- .NET ----------
WORKDIR /src/api
RUN dotnet restore "app/app.csproj"
RUN dotnet build "app/app.csproj" -c Release -o /app/build --no-restore
FROM build AS publish
WORKDIR /src/api
RUN dotnet publish "app/app.csproj" -c Release -o /app/publish /p:UseAppHost=false
Finally, copy the Angular build output into the ASP.NET application's wwwroot folder so that both parts are packaged into a single deployment artifact.
# Copy Angular to wwwroot
RUN mkdir -p /app/publish/wwwroot
RUN cp -R /src/ui/dist/*/browser/* /app/publish/wwwroot/ \
&& cp /app/publish/wwwroot/index.csr.html /app/publish/wwwroot/index.html
The final stage is to create the runtime image.
FROM base AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443
ENV ASPNETCORE_ENVIRONMENT=Production
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "app.dll"]
Create and push an image to Azure Container Registry
Building the Docker image locally and pushing it to Azure Container Registry (ACR) after every change is a perfectly valid approach while experimenting. However, it quickly becomes a repetitive manual task. If something can be automated, it is worth doing.
The GitHub Actions workflow performs the following steps:
- Check out the API repository into the
apidirectory. - Check out the UI repository into the
uidirectory. - Build the .NET API.
- Run all backend tests and generate code coverage reports.
- Build the Angular application.
- Run all frontend tests and generate code coverage reports.
- Authenticate with Azure.
- Authenticate with Azure Container Registry.
- Build the Docker image.
- Push the image to Azure Container Registry.
Although the workflow consists of many individual steps, once everything has been configured, the entire process becomes fully automated and requires no manual intervention.
Configure and start the application
For this project, Azure Container Apps was selected as the hosting platform for several reasons:
- It is specifically designed for running containerized applications, including scaling and environment configuration.
- It supports application revisions, making deployments much safer.
- It integrates seamlessly with Azure Container Registry, Log Analytics, Azure Storage, Azure Key Vault, and other Azure services.
- Application metrics and Grafana dashboards are available out of the box.
The initial configuration consists of several steps:
- Ingress. Enable ingress, configure the target port, select HTTP as the ingress type, and allow external traffic unless a different networking configuration is required.
-
Containers.
- Properties. Configure the Docker image to be used.
- Environment variables. Define the required configuration values. These can be populated directly from Azure secrets if desired.
- Health probes. Configure the Startup, Readiness, and Liveness probes by specifying the endpoint, port, and protocol. This is a critical step because if the probes fail, Azure Container Apps considers the container unhealthy and automatically restarts or disables it.
- Volume mounts. Attach Azure Storage volumes when the application requires persistent file storage.
It is worth noting that every time the Container App configuration is saved, Azure creates a new application revision and redeploys the container.
However, Azure Container Apps does not automatically detect that a newer image with the same tag (for example, latest) has been pushed to Azure Container Registry.
A simple workaround is to create an environment variable such as FORCE_REDEPLOY and use the current date or timestamp as its value. Whenever this value changes and the configuration is saved, Azure creates a new revision, pulls the latest Docker image, and starts a new container without requiring any other configuration changes.
Summary
The steps described above were sufficient to build an application from multiple repositories, package it into a single Docker image, publish it to Azure Container Registry, deploy it to Azure Container Apps, and finally obtain a stable, repeatable, and fully automated deployment pipeline.
While each individual step is relatively straightforward, combining them into a complete CI/CD process significantly simplifies future development. After the initial setup, every commit can automatically produce a tested Docker image that is ready to be deployed with minimal effort.