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:
A docker configuration file, “Dockerfile”, is created and it looks like this:
# For more info see: http:
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.
The added project is of type “.dcproj” and the following files are created:
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:
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:
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:
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"CarApiUrl": "http//localhost:54411/"
}
The environment
variable is read like this:
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:
version: '3.4'
services:
...
carclient:
image: ${DOCKER_REGISTRY}carclient
environment:
- CarApiUrl=http:
build:
context: .
dockerfile: CarClient/Dockerfile
The environment variable is read in file Utils.cs:
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
:
# For more info see: http:
FROM microsoft/aspnetcore:2.0 AS base
WORKDIR /app
EXPOSE 54411
...
In the docker-compose.yml, map external port 54411
to internal 80
:
version: '3.4'
services:
carapi:
image: ${DOCKER_REGISTRY}carapi
ports:
- 54411:80
...
The original JavaScript code is kept:
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:
…> docker-compose --no-ansi up -d --force-recreate --remove-orphans
Then check with docker ps
on which port carclient
is running:
…> 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)
>>docker kill $(docker ps -aq)
Start shell inside container
>>docker exec -i -t container_name /bin/bash
Run the solution without docker-compose but with docker networking
cd to carapi
>>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 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:
cd to carclient
>>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.