Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / containers / docker

Running ASP.NET Web API Solution in Docker

4.95/5 (16 votes)
6 Mar 2019CPOL5 min read 73.5K  
This article describes how to containerize an ASP.NET WebApi solution with Visual Studio, how to pass data via environment variables and also how to run the images in docker without Visual Studio.

Introduction

The tricky part when running a web solution with a web API in docker containers is to map the URLs and ports so that the code running inside the docker container can be accessed from outside. This is a question of docker configuration and minor code changes.

Background

This article is a contribution to the Docker Contest described in this article.

Prerequisites

  • Visual Studio 2017, community version. Latest release.
  • You have installed “Docker For Windows” on your computer: https://download.docker.com/win/stable/Docker for Windows Installer.exe.
  • You have an existing solution with a web API and a web “model-view-controller“ project and the MVC project is able to communicate with the web API via a REST http interface. If not, you may use the CarApi and CarClient projects (see below) to implement your own solution.

The code belonging to this article is the containerized versions of CarClient and CarApi from this article.

In this article, docker support has been added and the docker configuration files have been updated to make it possible to access the API from CarClient, both frontend and backend.

How to Containerize an Existing Project

To add docker support for an existing web project, e.g., CarApi, open the project in Visual Studio, right click the project and chose Add -> Docker Support:

1257705/Containerize.jpg

A docker configuration file, “Dockerfile”, is created and it looks like this:

Docker
# For more info see: http://aka.ms/VSContainerToolingDockerfiles
FROM microsoft/aspnetcore:2.0 AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/aspnetcore-build:2.0 AS builder
WORKDIR /src
COPY *.sln ./
COPY CarApi/CarApi.csproj CarApi/
RUN dotnet restore
COPY . .
WORKDIR /src/CarApi
RUN dotnet build -c Release -o /app

FROM builder AS publish
RUN dotnet publish -c Release -o /app

FROM base AS production
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "CarApi.dll"

Do this for both projects in your existing solution, i.e., for the web API and the web MVC project. When this is done, you need to add a docker-compose project to your solution.

Add a docker-compose Project

To add a docker-compose project to the solution, right click one of the projects and select Add -> Container Orchestrator Support -> Docker Compose -> Target OS:Linux.

1257705/Docker-compose.jpg

The added project is of type “.dcproj” and the following files are created:

1257705/Docker-composeFiles.jpg

The next step is to right click the other project and in the same way, select Add -> Container Orchestrator Support ->Docker Compose -> Target OS:Linux.

Suppose that your two projects are called “CarClient” and “CarApi”, then the resulting docker-compose.yml looks like this:

yml
version: '3.4'

services:
  web:
    image: ${DOCKER_REGISTRY}carclient
    build:
      context: .
      dockerfile: CarClient/Dockerfile

  api:
    image: ${DOCKER_REGISTRY}carapi
    build:
      context: .
      dockerfile: CarApi/Dockerfile

The Containerized Solution with docker-compose

After having added Dockerfiles to each project and the docker-compose project to the solution, the solution consists of three projects: A web MVC project, a web API project and a docker-compose project.

Add Environment Variable

To make the containerized version function, we need to make some configuration changes.

Backend

In the original CarClient project, the web API was reached via the following URL:

C#
private static readonly Uri Endpoint = new Uri("http://localhost:54411/");

Here, the URL is hard coded but we could also define an environment variable in launchSettings.json:

JSON
"profiles": {
  "IIS Express": {
    "commandName": "IISExpress",
    "launchBrowser": true,
    "environmentVariables": {
      "ASPNETCORE_ENVIRONMENT": "Development",
      "CarApiUrl": "http//localhost:54411/"
    }

The environment variable is read like this:

C#
var carApiUrl = Environment.GetEnvironmentVariable("CarApiUrl");

For the containerized solution, we use “dns discovery”. Docker networking, as well as kubernetes handles all this magic. Instead of localhost, the name of the service, as defined in the docker-compose, is used. To call the CarApi, use http://carapi. You don’t need to set the port number as the port number is an external attribute.

We will use the environment variable called CarApiUrl. This variable is defined in the docker-compose.yml file like this:

yml
version: '3.4'

services:
...
  carclient:
    image: ${DOCKER_REGISTRY}carclient
    environment:
      - CarApiUrl=http://carapi/
    build:
      context: .
      dockerfile: CarClient/Dockerfile

The environment variable is read in file Utils.cs:

C#
private static readonly Uri Endpoint = 
        new Uri(Environment.GetEnvironmentVariable("CarApiUrl"));

By using an environment variable, we don't need to change the code, only the configuration, when containerizing the solution.

Frontend

The JavaScript running in the browser uses port 54411. We must expose port 54411 by changing the docker configuration file for CarApi like this:

In the web API Dockerfile, write EXPOSE 54411:

docker
# For more info see: http://aka.ms/VSContainerToolingDockerfiles
FROM microsoft/aspnetcore:2.0 AS base
WORKDIR /app
EXPOSE 54411
...

In the docker-compose.yml, map external port 54411 to internal 80:

YAML
version: '3.4'

services:
  carapi:
    image: ${DOCKER_REGISTRY}carapi
    ports:
      - 54411:80
...

The original JavaScript code is kept:

JavaScript
xmlhttp.open("GET", "http://localhost:54411/api/car", true);

That’s all that's needed. You can now run your containerized solution in Visual Studio.

Run Your App without Visual Studio

Rebuild your solution with Visual Studio with the Release configuration. Run the docker-compose project with F5 to ensure the images are updated.

Outside of Visual Studio, you’ll need to use the docker-compose command rather than docker run. In PowerShell, cd to the solution folder where docker-compose.yml is located. Then run the docker-compose command like this:

BAT
…> docker-compose --no-ansi up -d  --force-recreate --remove-orphans

Then check with docker ps on which port carclient is running:

BAT
…> docker ps

CONTAINER ID  IMAGE     COMMAND                CREATED      STATUS      PORTS   …

5c5c3a6fa376  carclient "dotnet CarClient.dll" 12 hours ago Up 12 hours 0.0.0.0:32781->80/tcp

20cf31344091  carapi    "dotnet CarApi.dll"    12 hours ago Up 12 hours 54411/tcp, ….

Carclient runs on port 32781. The app will then become accessible at http://localhost:32781.

Explanation

If the Debug configuration is set, then empty non-workable images are created by Visual Studio. It manually maps the empty container to the filesystem to make possible debugging, "Edit and Continue" features and so on. Therefore, dev image is useless without Visual Studio. Build the image in the Release configuration to make it usable.

The full publishing process is described in the documentation: Visual Studio Tools for Docker.

Run Your App with Docker Networking

It’s possible to make containers communicate via docker networking without YAML.

First some useful docker commands:

Kill all, (start, run, rm all)

BAT
>>docker kill $(docker ps -aq)

Start shell inside container

BAT
>>docker exec -i -t container_name /bin/bash

Run the solution without docker-compose but with docker networking

BAT
cd to carapi
BAT
>>docker build -t carapi .

>>docker run -e ASPNETCORE_ENVIRONMENT=Development -d -p 54411:80 --name carapi carapi

Inspect the docker bridge network to find the IP address used by carapi;

Docker
>> docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "fce049eb23e5fb1a7b5c801a082d8809efd4d369f18de4693b35e6524f1d55c0",
        "Created": "2019-03-04T15:13:40.1528498Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "cbf2ae63374659b4a9c8e341e22b20a7f3e6d7b6593d289d7ffa4db415d6e8b6": {
                "Name": "carapi",
                "EndpointID": "0c25cdaa8403c928b12efc1e5f6bb40b71e76acc438f8b95d2ba7a135eb333e9",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "f1abe1fdb72a23b61f7160aa49aca06f9c849dd3fca9432257168175625589a2": {
                "Name": "carclient",
                "EndpointID": "aa4dd99b8045e5a0c77f6dadd17ad9d3579c82c3cf7ee2af45461e6284523739",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

You see that carapi is using IP: 172.17.0.3. Use this IP number for the environment variable CarApiUrl in the docker run command below:

BAT
cd to carclient
BAT
>>docker build -t carclient .

>>docker run -e ASPNETCORE_ENVIRONMENT=Development 
  -e CarApiUrl=http://172.17.0.3 -d -p 8080:80 --name carclient carclient

Then start client in a browser with localhost:8080.

Now the containers communicate without YAML using the docker bridge network.

Points of Interest

In this article, we've discussed how to containerize an ASP.NET WebApi solution with Visual Studio, how to pass data via environment variables and also how to run the images in docker without Visual Studio. We also see how to use docker networking as an alternative to docker-compose and YAML files.

History

  • 24th August, 2018 - First revision of this article was published
  • 1st September, 2018 - Environment variable was introduced
  • 6th September, 2018 - Run the app without Visual Studio
  • 6th March, 2019 - Run app with docker networking without docker-compose

The code is available here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)