Containerizing an Angular And .NET Application With Docker

Packaging an Angular frontend and a .NET API into Docker containers gives a project a consistent runtime from a developer laptop to a staging server. Instead of asking every contributor to install the same Node.js, .NET SDK, database tools, and native dependencies, the team can describe those requirements as repeatable build instructions.

This approach is particularly useful when a project moves between local development environments. A developer in Sydney, a tester in Melbourne, and a production workload hosted in an Australian cloud region can use the same container images, while environment-specific settings remain outside the image.

The goal is not to put every part of an application into one oversized container. A practical arrangement uses a multi-stage image for Angular, a separate image for ASP.NET Core, and Docker Compose to connect them during development. This keeps the boundaries clear and makes each service easier to build, test, monitor, and deploy.

Why Containers Suit Angular And .NET

An Angular application is compiled into static HTML, CSS, JavaScript, and asset files. Once the production build is complete, it does not need Node.js to serve those files. A lightweight web server such as Nginx can handle the frontend, while the .NET application runs in its own ASP.NET Core runtime container.

The API has a different lifecycle. It may connect to SQL Server, validate authentication tokens, process business rules, and expose JSON endpoints. Keeping it separate means the frontend can be rebuilt without repackaging the API. It also allows each service to scale independently if the application grows.

Containers reduce the “works on my machine” problem, though they do not remove configuration concerns. Image tags, database migrations, secrets, file permissions, and network settings still need deliberate treatment. Docker is most valuable when the project uses it as an executable description of the application rather than as a shortcut around deployment design.

For Australian teams, regional hosting can be part of that design. Azure, AWS, and other providers offer infrastructure in or near Sydney, which can help with latency and data residency expectations. A container image remains portable, allowing the same application to be tested locally before it is deployed to a Sydney region or a private environment.

Preparing The Project Layout

A simple repository might contain an Angular workspace, an ASP.NET Core API, and a Compose file at the root:

my-app/
├── client/
│   ├── package.json
│   ├── angular.json
│   └── src/
├── server/
│   ├── MyApp.Api.csproj
│   ├── Program.cs
│   └── Controllers/
├── docker-compose.yml
└── .dockerignore

The .dockerignore files are as important as the Dockerfiles. Excluding node_modules, Angular’s dist directory, .git, local secrets, test output, and build artefacts keeps the build context small. A smaller context improves build speed and avoids accidentally copying credentials into an image layer.

Place a .dockerignore inside client and server when each directory is used as a separate build context. Typical Angular exclusions include node_modules, dist, .angular, and coverage output. The .NET version should exclude bin, obj, user secrets, and local database files.

The Angular application should call the API through a configurable base URL rather than embedding a machine-specific address. During development, that might be an Angular proxy targeting http://localhost:5000. In a production-style Compose setup, Nginx can proxy /api to the service name api, keeping browser requests on one origin and avoiding many CORS complications.

Building The Angular Image

A multi-stage Dockerfile compiles the Angular project with Node.js, then copies only the generated files into an Nginx image:

FROM node:22-alpine AS build
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build -- --configuration production

FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/my-app/browser /usr/share/nginx/html
EXPOSE 80

The exact dist path depends on the Angular version and project configuration. Some workspaces produce dist/my-app, while newer application builders may place browser output in dist/my-app/browser. Confirm the path by running a local production build before finalising the image.

npm ci is preferable to npm install in a reproducible build because it uses the lock file and fails when the dependency definitions are inconsistent. Pinning the Node and Nginx major versions also makes upgrades intentional. The image can later be rebuilt with a newer patch version after the change has been tested.

Angular routing needs a fallback so that a direct request to /orders/42 returns index.html instead of a 404. A small Nginx configuration handles that and can proxy API calls:

server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://api:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

The api hostname is resolved through the Docker Compose network. From a browser running on the host, localhost is still the relevant address; from the Nginx container, api is the correct destination. Confusing those two perspectives is a common source of connection errors.

Packaging The ASP.NET Core API

The .NET image follows the same multi-stage pattern: restore dependencies, compile the application, publish it, and run the published output with the smaller ASP.NET Core runtime image.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY ["MyApp.Api.csproj", "./"]
RUN dotnet restore "MyApp.Api.csproj"

COPY . .
RUN dotnet publish "MyApp.Api.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]

The project file is copied before the remaining source code so Docker can cache the restore layer. If only a controller changes, the dependency restore does not need to run again. This small ordering choice becomes valuable in CI pipelines.

The API should listen on 0.0.0.0 inside the container, not only on localhost. ASPNETCORE_URLS=http://+:8080 makes the service reachable through the Docker network. Port publishing is a separate concern: Compose can map host port 5000 to container port 8080 for direct local access, while Nginx can communicate internally without exposing the API publicly.

Configuration should come from environment variables or mounted configuration providers. Connection strings, JWT signing keys, and third-party credentials do not belong in the Dockerfile. ASP.NET Core maps variables such as ConnectionStrings__Default to nested configuration keys, making it straightforward to supply different settings for a local database, test environment, and production deployment.

Connecting Services With Compose

Docker Compose describes how the frontend, API, and supporting services run together. A basic file might look like this:

services:
  api:
    build:
      context: ./server
    environment:
      ASPNETCORE_ENVIRONMENT: Development
      ASPNETCORE_URLS: http://+:8080
    ports:
      - "5000:8080"

  web:
    build:
      context: ./client
    ports:
      - "4200:80"
    depends_on:
      - api

Running docker compose up --build builds both images and starts the containers. The Angular site is available at http://localhost:4200, and the API can be reached directly at http://localhost:5000 when that port mapping is retained. If Nginx proxies /api, the frontend can use relative requests such as /api/products.

depends_on controls startup order, but it does not guarantee that the API is ready to accept requests. Add a health check when the frontend or another service must wait for a functioning dependency. For a database, health checks are especially helpful because a container can be running while the database engine is still initialising.

A SQL Server container can be added for local development, though it needs a licence-appropriate image, an accepted end-user agreement, a strong local password, and a persistent volume. Teams in Brisbane or Perth can share the same Compose file with colleagues in Canberra without relying on locally installed database versions. Production should use a managed database or a separately operated database service rather than treating a development container as durable storage.

Use docker compose logs -f api and docker compose ps as first diagnostic steps. If the browser reports a network failure, inspect the Nginx logs, then test the API from inside the web container. This quickly distinguishes an Angular configuration issue from a Docker DNS or .NET binding issue.

Making The Container Setup Production Ready

A development Compose file is a useful starting point, but production requires stronger controls. Run containers as non-root users where possible, keep the base images current, scan dependencies, and avoid copying source maps or development configuration into a public image. Multi-stage builds already help by leaving compilers and package managers out of the final runtime layers.

Health endpoints give an orchestrator a reliable way to determine whether the API is alive and ready. A liveness check can confirm that the process responds, while a readiness check can verify critical dependencies such as SQL Server. Add structured logs that write to standard output so Docker, Kubernetes, or a cloud logging service can collect them.

Environment configuration also deserves careful thought in Angular. Values compiled into a frontend bundle are public, even if they came from a build secret. API URLs, feature flags, and public analytics identifiers can be injected at build time, but private keys must remain on the server. Review the generated JavaScript before release and treat everything delivered to a browser as visible to its users.

The same review should cover links and external content. If an Angular component renders editorial or promotional references, apply a safe allow-list, use suitable rel attributes for untrusted destinations, and test the resulting navigation; a page such as this external content example illustrates why outbound URLs should be handled deliberately rather than inserted blindly into templates.

Australian deployments may also need consistent time handling. Store timestamps in UTC, format them for users in AEST or AEDT at the presentation layer, and test daylight-saving transitions for customers in Sydney and Melbourne. Currency and date formatting should likewise be explicit, especially when an application serves both Australian and overseas customers.

Practical Recommendations For A Reliable Setup

A containerised Angular and .NET application should be easy to build from a clean checkout, start with one documented command, and diagnose through observable logs and health checks. Begin by splitting the frontend and API, add multi-stage builds, connect them with Compose, and then move secrets, persistence, TLS, and orchestration into the deployment platform.

Run the complete stack locally, verify it with automated tests, and publish the same tested images to your chosen registry. That workflow gives a development team a dependable path from a laptop in Adelaide or Sydney to a production service running in an Australian cloud region.