Introduction

In this blog post we will see how to dockerize ASP.NET Core application, which connects to a MSSQL database and uses Vue.js as front-end. After that we will use them together with Docker Compose. We will use these projects in this tutorial.

I have to admit it was harder than I thought it would be.

Backend Application

Let’s start with exploring our backend application.

It consists of a single endpoint which returns information about a phone book entry:

public class PhoneBookEntryModel
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string PhoneNumber { get; set; }

    public string Address { get; set; }

}

This data is stored in an MSSQL database. Upon the first start of the application, it checks if there is a database named PhoneBook and if there isn’t, it is created automatically. After the creation of the database some sample data is inserted inside the database so we have something to work with.

Here is the backend application if you want to check it out.

Database Schema

There is not much to say actually. It is just 1 table to keep things simple:

DB_SCHEMA

Front End Applicaiton

The front-end is developed using the latest Vue.js which is version 3 and the composition api. It consists of a single component which displays the data that is fetched from the backend.

In order to specify backend address dynamically this example takes advantage of the new Vue.js client-side environment variables. All you have to do is create a .env file put your environment variable with the default value there and access it with process.env.[VARIABLE NAME].

Here is the front-end application if you want to check it out

Folder Structure

One of the first things you should pay attention is the folder structure. You will write your Dockerfiles based on the location of your docker-compose file. I find it best to put the docker-compose.yml on the root folder so it will be easier to locate the Dockerfiles. Here is how my folder structure looks like.

.
├── Backend
│   ├── ...
│   ├── .dockerignore
│   └── Dockerfile
├── front-end
│   ├── ...
│   ├── .dockerignore
│   └── Dockerfile
└── docker-compose.yml

The docker-compose.yml is stored at the root folder and inside the concrete folders is the location of the Dockerfiles for the specific project.

Dockerize ASP.NET Core application

For those of you that cannot wait to see the Dockerfile here it is:

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env
WORKDIR /app

COPY Backend/*.sln ./
COPY Backend/Backend/*.csproj ./Backend/
COPY Backend/DAL/*.csproj ./DAL/
RUN dotnet restore

COPY Backend/ ./
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "Backend.dll"]

Let’s start from the top

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env
WORKDIR /app

The first row sets the base image. Every ASP.NET release comes with a base image which you can check here. Once the base image is established, we define a folder as our working directory which we will store our files.

COPY Backend/*.sln ./
COPY Backend/Backend/*.csproj ./Backend/
COPY Backend/DAL/*.csproj ./DAL/
RUN dotnet restore

This piece of code is purely for Docker’s caching mechanism. It tells Docker that if we did not modify the csproj files to not make a restore which significantly improves the build time of the docker image.

COPY Backend/ ./
RUN dotnet publish -c Release -o out

Copy the rest of the files and publish the packages in release configuration.

FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build-env /app/out .

Here we specify the second image taking advantage of Docker layers. In context of ASP.NET Core, this helps reduce image size dramatically.

In first Docker base image we have the .NET SDK inside which helps us to publish our application. Once our application is published, we do not need the whole SDK, only the runtime. This is where the second base image comes. The second base image contains only the runtime which reduces the image size. After that, Docker uses only the second part of the Dockerfile discarding the first layer.

So how can we test if our Dockerfile is able to dockerize our application? If you go to the folder and run:

docker build .

You will get an error like the following

ERROR [build-env 4/8] COPY Backend/Backend/*.csproj ./Backend/

That is because Docker cannot find our files. Let me remind you how our folder structure looked like:

.
├── Backend
│   ├── ...
│   ├── .dockerignore
│   └── Dockerfile
├── front-end
│   ├── ...
│   ├── .dockerignore
│   └── Dockerfile
└── docker-compose.yml

We have to execute the command from our base location. The location which our docker-compose.yml file will be. After navigating there from the command line, we need to specify the docker file explicitly:

 docker build -f "./Backend/Dockerfile" .

Don’t forget the .dockerignore file. Believe me you do not want all that bin and obj folders inside your container:

bin/
obj/
.vs/

Now everything should be working fine and you should have a Docker image.

Dockerize Vue.js application

Again, for those of you that cannot wait the Dockerfile here it is:

FROM node:lts-alpine as build-stage

ARG VUE_APP_API_URL
ENV VUE_APP_API_URL ${VUE_APP_API_URL}

WORKDIR /app
COPY ./front-end/package*.json .
RUN npm install

COPY ./front-end/ .
RUN npm run build

# production stage
FROM nginx:stable-alpine as production-stage

COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Nothing special in the first line.

FROM node:lts-alpine as build-stage

It sets our base image which in this case it is alpine based node. What follows is more interesting.

ARG VUE_APP_API_URL
ENV VUE_APP_API_URL ${VUE_APP_API_URL}

Since we take advantage of the Vue.js’s client-side environment variables we need a way to tell Docker to supply these variables. The easiest way to do this is to define environment variables which are set from arguments. Be careful though, most people do not prefer this approach. The image could differ from environment to environment because you can only set the env variables in build time. If you want alternative check this article from dev.to.

WORKDIR /app
COPY ./front-end/package*.json .
RUN npm install

The same caching principle as with ASP.NET Core application applies here too. First, we only copy both package.json and package-lock.json for Docker to be able to cache it which will improve our image build time.

COPY ./front-end/ .
RUN npm run build

After that we copy the rest and install the application.

# production stage
FROM nginx:stable-alpine as production-stage

COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Finally in order to drastically reduce the image size we define a new layer with new base image which consists of only the necessary parts.

Docker Compose the Applications

The last piece of our puzzle is to assemble the Docker Compose file. Here is how it looks like complete:

services:
  db:
    image: mcr.microsoft.com/mssql/server:2019-latest
    ports:
      - "1433:1433"
    environment:
      SA_PASSWORD: qUgeyvYnOKyMy9HRpGMauKlJdsyq1QdIJUtp8Hz6HarKKCIiVm7m3ITHsBFQSMkEcrGXQy4FNHFX6zKq5NTzysbE0EVJnh3HZwl6DkAloQu7GQ1NZVVNnYEFpkAcxdun
      ACCEPT_EULA: "Y"

  phone-book-api:
    image: phone-book-api
    restart: unless-stopped
    build:
      context: .
      dockerfile: ./Backend/Dockerfile
    environment:
      - ConnectionStrings__PhoneBook=Server=db;Database=PhoneBook;Trusted_Connection=False;User Id=sa;Password=qUgeyvYnOKyMy9HRpGMauKlJdsyq1QdIJUtp8Hz6HarKKCIiVm7m3ITHsBFQSMkEcrGXQy4FNHFX6zKq5NTzysbE0EVJnh3HZwl6DkAloQu7GQ1NZVVNnYEFpkAcxdun;MultipleActiveResultSets=true
      - Logging__LogLevel__Default=Information
    ports:
      - "8080:80"
    depends_on:
      - db

  phone-book-front-end:
    image: phone-book-front-end
    restart: unless-stopped
    build:
      context: .
      dockerfile: ./front-end/Dockerfile
      args:
        VUE_APP_API_URL: http://localhost:8080
    ports:
      - "8081:80"
    depends_on:
      - phone-book-api

The Database

First thing we need in order everything to work fine is our database. The only thing you should be careful here is what password you choose. If you choose a weak password the deployment will fail and you have to check the logs manually in order to find out what went sideways. If you choose to have special symbols in the password the deployment will fail again if you do not escape it properly.

The Backend

There are several gotchas here that you should be aware of.

Pay attention to the restart element in the yaml file:

restart: unless-stopped

The purpose of this element is pretty clear, to restart the application if it fails to start. But why it will fail? Because the application starts before the database, after that it crashes and you do not have a backend unless you specified that it should be restarted. In that case why we have:

    depends_on:
      - db

I am glad you asked. This tag just tells Docker Compose to prioritize the database and start it before the backend. What it does not tell is to wait for the database to be ready to accept connections. Which means that you have a database that is not ready and a crashing application. This is the easiest way to get rid of this problem. If you want more professional looking solution you can check this excellent github repo which represents a script that makes your application wait for the database to be ready before the first start.

The other thing that could cause a headache is the connection string. Let’s check the appSettings.json of the Backend to see how our connection string looks like:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "PhoneBook": "Data Source=.;Initial Catalog=PhoneBook;"
  }
}

Our connection string is nested inside ConnectionString. We cannot just copy paste this in Docker as it is. Fortunately, Docker has a neat syntax to do that. The hierarchies are implemented with 2 underscores __. Which makes our connection string to look like the following:

ConnectionStrings__PhoneBook=Server=db;Database=PhoneBook;Trusted_Connection=False;User Id=sa;Password=...

Pay attention how our connection string itself looks like:

Server=db;Database=PhoneBook;Trusted_Connection=False;User Id=sa;Password=...

As a server we specify the name of our Database service which is db.

The Front End

Fortunately, front end application is pretty straightforward. The only thing you should be aware of is how to pass dynamic server address:

args:
   VUE_APP_API_URL: http://localhost:8080

Build and Start

After all this trouble we are finally ready to build our applications and start the project:

docker-compose build
docker-compose up

Conclusion

In this article we saw how to dockerize ASP.NET Core application and Vue.js application. We covered the Vue.js client-side environment variables and how to use them with Docker. Finally, we composed all applications and forced them to work together.

What part of all this gave you the hardest time? Tell me on twitter.