9

Adding Docker to the ASP.NET Core Angular Template

 3 years ago
source link: https://espressocoder.com/2020/02/05/adding-docker-to-the-asp-net-core-angular-template/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Adding Docker to the ASP.NET Core Angular Template

What’s better than starting a new greenfield project? You finally have the opportunity to leverage all the latest patterns, technologies, and frameworks that you’ve been dying to get your hands on. A project I started recently had a strong focus on Docker. Being a new project, we were also able to use the latest version of ASP.NET Core and Visual Studio 2019. If you’ve read any of my previous posts, you know that .NET Core and Visual Studio have many very convenient interaction points with Docker.

Things that would typically take a couple of hours to setup are created with the click of a button. This is, of course, until you use the ASP.NET Core Angular template and notice the Enable Docker Support checkbox is disabled. If only life were so simple. Fortunately, with a couple of small changes, we can incorporate Docker into the default ASP.NET Core Angular template and take advantage of the benefits that come along with the Visual Studio container tools.

This article will discuss enhancing the default template configuration to build and run your ASP.NET Core Angular app in Docker.

update-1672363_1280-1024x682.jpg

The Angular Project Template

When we create a new ASP.NET Core Web Application using the Angular template, a new “Hello, World” application is generated. In addition to the Angular frontend, we also have an ASP.NET Core API setup server-side. Debugging the project in Visual Studio, we will notice both the Angular and ASP.NET Core applications are running together.

If you’ve worked on a project like this before, you are probably used to debugging your frontend with the Angular CLI and your API with Visual Studio. The Angular project template appears to be doing both for us! But how? We can see this by looking at the Startup.cs file.

public class Startup
// Removed for brevity
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
// Removed for brevity
app.UseSpa(spa =>
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
spa.UseAngularCliServer(npmScript: "start");
public class Startup
{
    // Removed for brevity

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // Removed for brevity

        app.UseSpa(spa =>
        {
            // To learn more about options for serving an Angular SPA from ASP.NET Core,
            // see https://go.microsoft.com/fwlink/?linkid=864501

            spa.Options.SourcePath = "ClientApp";

            if (env.IsDevelopment())
            {
                spa.UseAngularCliServer(npmScript: "start");
            }
        });
    }
}

As we can see, npm start is getting executed under the hood by leveraging the Microsoft.AspNetCore.SpaServices nuget package. Looking at our package.json file, we can see this is effectively calling ng serve.

"name": "jrtech.angular.docker",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build:ssr": "ng run JrTech.Angular.Docker:server:dev",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
{
  "name": "jrtech.angular.docker",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "build:ssr": "ng run JrTech.Angular.Docker:server:dev",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  ...

}

All this bridges the frontend and backends nicely; however, it doesn’t come without some downsides. First and foremost, when a backend change is made, ng serve needs to run again, which can take 10+ seconds depending on the frontend application size. Microsoft mentions this in their documentation and offers a convenient workaround, as shown in the Startup class below.

app.UseSpa(spa =>
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
// spa.UseAngularCliServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
app.UseSpa(spa =>
{
    // To learn more about options for serving an Angular SPA from ASP.NET Core,
    // see https://go.microsoft.com/fwlink/?linkid=864501

    spa.Options.SourcePath = "ClientApp";

    if (env.IsDevelopment())
    {
        // spa.UseAngularCliServer(npmScript: "start");
        spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
    }
});

Making this change allows us to debug our Angular application from the command-line while using Visual Studio to debug our ASP.NET Core API.

Introducing Docker

While we have an excellent development environment setup, let’s revisit the article’s beginning. On this new project, we wanted to leverage Docker as much as possible. So how does Docker fit into our new project? Well, at the moment it doesn’t, but we can fix that!

Just like anything else, we have a couple of options. For example, we could take the entire project and throw it into a container. This strategy would work; however, it will have the same issues as previously discussed. Any changes to the backend code would require us to rebuild the backend AND frontend projects and vise versa. To build them independently, we will have to split them into separate containers.

First, let us take a look at running our ASP.NET Core API in Docker.

ASP.NET Core With Docker

Even though we can’t include Docker support when we create an Angular project, we can still add Docker support afterward from the project menu. This will automatically generate a Dockerfile similar to the one shown below.

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["JrTech.Angular.Docker/JrTech.Angular.Docker.csproj", "JrTech.Angular.Docker/"]
RUN dotnet restore "JrTech.Angular.Docker/JrTech.Angular.Docker.csproj"
COPY . .
WORKDIR "/src/JrTech.Angular.Docker"
RUN dotnet build "JrTech.Angular.Docker.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "JrTech.Angular.Docker.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "JrTech.Angular.Docker.dll"]
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["JrTech.Angular.Docker/JrTech.Angular.Docker.csproj", "JrTech.Angular.Docker/"]
RUN dotnet restore "JrTech.Angular.Docker/JrTech.Angular.Docker.csproj"
COPY . .
WORKDIR "/src/JrTech.Angular.Docker"
RUN dotnet build "JrTech.Angular.Docker.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "JrTech.Angular.Docker.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "JrTech.Angular.Docker.dll"]

At this point, if we try to debug our project, the browser will display a “Failed to proxy the request to http://localhost:4200/” error message. As the error message states, our ASP.NET project is unable access our Angular application. Since ASP.NET Core is now running in a container, we will get the same error message even if ng serve is running locally. We can solve this by continuing our with our journey into Docker.

Angular With Docker

The next step is to configure our Angular application to run in Docker as well. For this, we will create a separate Dockerfile. This gives us the flexibility to stop and start our API without rebuilding the frontend. I like to use a Dockerfile similar to the one shown below.

FROM node:10.15-alpine AS client
EXPOSE 4200 49153
USER node
RUN mkdir /home/node/.npm-global
ENV PATH=/home/node/.npm-global/bin:$PATH
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
RUN npm install -g @angular/[email protected]
WORKDIR /app
CMD ["ng", "serve", "--port", "4200", "--host", "0.0.0.0", "--disable-host-check", "--poll", "2000"]
FROM node:10.15-alpine AS client
EXPOSE 4200 49153
USER node

RUN mkdir /home/node/.npm-global
ENV PATH=/home/node/.npm-global/bin:$PATH
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global

RUN npm install -g @angular/[email protected]

WORKDIR /app
CMD ["ng", "serve", "--port", "4200", "--host", "0.0.0.0", "--disable-host-check", "--poll", "2000"]

The resulting Docker image image will contain node, npm, and the Angular CLI. When the container is started, ng server will be executed in the app folder. Of course, the app folder will be empty, but we can make our code accessible to that location by mounting a volume.

Bringing it all Together

To run these two containers together, we use a tool that goes hand-in-hand with Docker called Docker Compose. If you are not familiar with Docker Compose, it is a container orchestration tool that is used for configuring and running multi-container Docker applications. When using Docker Compose, everything gets configured in a YAML configuration file. We can create a docker-compose.yml file in Visual Studio by right-clicking the project and selecting Add | Container Orchestration Support.

After it is created, our Angular container will need to be included, as shown below.

version: '3.4'
services:
jrtech.angular.docker:
image: ${DOCKER_REGISTRY-}jrtechangulardocker
build:
context: .
dockerfile: JrTech.Angular.Docker/Dockerfile
jrtech.angular.app:
image: ${DOCKER_REGISTRY-}jrtechangularapp
build:
context: .
dockerfile: JrTech.Angular.Docker/ClientApp/Dockerfile
ports:
- "4200:4200"
- "49153:49153"
volumes:
- ./JrTech.Angular.Docker/ClientApp:/app
version: '3.4'

services:
  jrtech.angular.docker:
    image: ${DOCKER_REGISTRY-}jrtechangulardocker
    build:
      context: .
      dockerfile: JrTech.Angular.Docker/Dockerfile

  jrtech.angular.app:
    image: ${DOCKER_REGISTRY-}jrtechangularapp
    build:
      context: .
      dockerfile: JrTech.Angular.Docker/ClientApp/Dockerfile
    ports:
      - "4200:4200" 
      - "49153:49153"
    volumes:
      - ./JrTech.Angular.Docker/ClientApp:/app
    

Lastly, we will need to update our Startup.cs file to proxy requests to the Angular container. We use the service name (jrtech.angular.app) in order to work with Docker’s internal network.

app.UseSpa(spa =>
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
spa.UseProxyToSpaDevelopmentServer("http://jrtech.angular.app:4200");
app.UseSpa(spa =>
{
    // To learn more about options for serving an Angular SPA from ASP.NET Core,
    // see https://go.microsoft.com/fwlink/?linkid=864501

    spa.Options.SourcePath = "ClientApp";

    if (env.IsDevelopment())
    {
        spa.UseProxyToSpaDevelopmentServer("http://jrtech.angular.app:4200");
    }
});

Voila! Now if we set the docker-compose project as our startup project and debug, both containers will start running. If we make some changes and rerun the debugger, it will be fast as the Angular container never stops!

Deploying as a Single Container

We now have an excellent development process setup; however, we may not want to deploy in this configuration. While nice for local development, deploying our frontend and backend applications separately means we will have to deal with CORS, preflight requests, etc. If we want to avoid this, we can support a single container deployment by making a few tweaks to our Dockerfile.

By default, Visual Studio creates a multistage Dockerfile. We can extend this by adding an additional stage for our Angular application. We also introduce a build-time argument that will dictate if we want to build the frontend components or not.

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM node:10.15-alpine AS client
ARG skip_client_build=false
WORKDIR /app
COPY JrTech.Angular.Docker/ClientApp .
RUN [[ ${skip_client_build} = true ]] && echo "Skipping npm install" || npm install
RUN [[ ${skip_client_build} = true ]] && mkdir dist || npm run-script build
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["JrTech.Angular.Docker/JrTech.Angular.Docker.csproj", "JrTech.Angular.Docker/"]
RUN dotnet restore "JrTech.Angular.Docker/JrTech.Angular.Docker.csproj"
COPY . .
WORKDIR "/src/JrTech.Angular.Docker"
RUN dotnet build "JrTech.Angular.Docker.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "JrTech.Angular.Docker.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY --from=client /app/dist /app/dist
ENTRYPOINT ["dotnet", "JrTech.Angular.Docker.dll"]
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM node:10.15-alpine AS client 
ARG skip_client_build=false 
WORKDIR /app 
COPY JrTech.Angular.Docker/ClientApp . 
RUN [[ ${skip_client_build} = true ]] && echo "Skipping npm install" || npm install 
RUN [[ ${skip_client_build} = true ]] && mkdir dist || npm run-script build

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["JrTech.Angular.Docker/JrTech.Angular.Docker.csproj", "JrTech.Angular.Docker/"]
RUN dotnet restore "JrTech.Angular.Docker/JrTech.Angular.Docker.csproj"
COPY . .
WORKDIR "/src/JrTech.Angular.Docker"
RUN dotnet build "JrTech.Angular.Docker.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "JrTech.Angular.Docker.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY --from=client /app/dist /app/dist
ENTRYPOINT ["dotnet", "JrTech.Angular.Docker.dll"]

We give the argument a default value of false, but in our docker-compose.yml file, we override it to true. This way, when we build our containers with docker-compose, the Angular application will not be redundantly built inside of the ASP.NET Core container.

version: '3.4'
services:
jrtech.angular.docker:
image: ${DOCKER_REGISTRY-}jrtechangulardocker
build:
context: .
dockerfile: JrTech.Angular.Docker/Dockerfile
args:
- skip_client_build=true
jrtech.angular.app:
image: ${DOCKER_REGISTRY-}jrtechangularapp
build:
context: .
dockerfile: JrTech.Angular.Docker/ClientApp/Dockerfile
ports:
- "4200:4200"
- "49153:49153"
volumes:
- ./JrTech.Angular.Docker/ClientApp:/app
version: '3.4'

services:
  jrtech.angular.docker:
    image: ${DOCKER_REGISTRY-}jrtechangulardocker
    build:
      context: .
      dockerfile: JrTech.Angular.Docker/Dockerfile
      args:
        - skip_client_build=true

  jrtech.angular.app:
    image: ${DOCKER_REGISTRY-}jrtechangularapp
    build:
      context: .
      dockerfile: JrTech.Angular.Docker/ClientApp/Dockerfile
    ports:
      - "4200:4200" 
      - "49153:49153"
    volumes:
      - ./JrTech.Angular.Docker/ClientApp:/app

Lastly, we can remove the majority of the custom build configuration in our project file as all of our Angular build steps are now in Docker. I cleaned mine up to look very similar to a typical ASP.NET Core web application.

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<UserSecretsId>9803d20b-7c4b-45ad-b021-58160cf46b32</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.9.10" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
    <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
    <UserSecretsId>9803d20b-7c4b-45ad-b021-58160cf46b32</UserSecretsId>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.0" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.9.10" />
  </ItemGroup>

</Project>

With this configuration, when running our application with docker-compose our Angular app is built in a separate container. When the container is built individually, all of the Angular components are self-contained within the ASP.NET Core container. This gives us the ability to build, compile, and debug these applications while developing and generate a self-contained image for deployment.

How are you leveraging Docker with the ASP.NET Core Angular template?

Like this:

Loading...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK