3

I have two projects:

  1. UI (a .NET Core 3.1 MVC font-end)
  2. API (a .NET Core 3.1 Web API)

that each run in two separate containers. They share a docker-compose file that builds them both. From the UI project, I want to be able to make an AJAX call to hit the endpoint in the API project. What is the URL that will hit my endpoint?

What I have tried:

  1. If I run the API project alone with IIS, I can successfully hit the endpoint by navigating to https://localhost:49239/weatherforecast. But, again, I want to hit this endpoint by calling it from the client within the UI project.
  2. If I use the docker-compose to launch both containers, then from the UI project click the button that executes my AJAX call to the endpoint https://localhost:49239/weatherforecast it does not work. I have tried many variations to the host part of the URL to reach it.

According to Networking in Compose

Compose sets up a single network for your app. Each container for a service joins the default network and is both reachable by other containers on that network and is discoverable by them at a hostname identical to the container name.

With that, I have also tried many variations of host URL such as https://api:49243/weatherforecast where "api" is the name of my docker image and "49243" is the port listed by docker ps. I have also tried:

  • https://api:80/weatherforecast
  • https://api:433/weatherforecast
  • https://api:PORT_NUM/weatherforecast where "PORT_NUM" is any port number listed for the container when viewing with docker ps

So how do I hit this endpoint???

Note:

  • I have run docker inspect CONTAINER_NAME and I know that both containers are on the same network.

Files:

  1. UI > Index.cshtml:
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
    <button type="button" class="btn btn-primary" onclick="callAPI()">Call API</button>
</div>

@section scripts {
    <script>
        function callAPI() {
            console.log("calling API...");

            $.ajax({
                url: `https://api:49221/weatherforast/get`,
                method: 'GET',
                success: function (data) {
                    console.log(data);
                },
                error: function (error) {
                    console.log(error);
                }
            });
        }
    </script>
}
  1. UI > Dockerfile:
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 ["UI/UI.csproj", "UI/"]
RUN dotnet restore "UI/UI.csproj"
COPY . .
WORKDIR "/src/UI"
RUN dotnet build "UI.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "UI.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "UI.dll"]
  1. API > WeatherForecastController.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace API.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}
  1. API > Dockerfile:
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 ["API/API.csproj", "API/"]
RUN dotnet restore "API/API.csproj"
COPY . .
WORKDIR "/src/API"
RUN dotnet build "API.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "API.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "API.dll"]
  1. docker-compose:
version: '3.4'

services:
  ui:
    image: ${DOCKER_REGISTRY-}ui
    build:
      context: .
      dockerfile: UI/Dockerfile

  api:
    image: ${DOCKER_REGISTRY-}api
    build:
      context: .
      dockerfile: API/Dockerfile
  1. To better understand the solution structure, here is a screen shot of my Solution Explorer
Joshua Abbott
  • 342
  • 1
  • 3
  • 11
  • Docker container is isolated and doesnt have access to your localhost. Are you maybe trying to do something like this https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach – AbdulG Jan 11 '21 at 19:32

2 Answers2

2

Ultimately I found that Visual Studio had created a file that I didn't notice for a while called docker-compose.override.yml. This file exposed ports 80 and 443 without specifying a host port, which I believe is what resulted in my app running at a random port number each run. This made it difficult for me to know where to point my API call.

Essentially, by simply adding the ports to my docker-compose.yml (as seen in the code block below) and (importantly) removing the ports sections from each service in the docker-compose.override.yml I was able to get the UI and WebAPI apps to each run on a specified port. From there, I needed to add the host name "https://localhost:49254" to my CORS policy in my WebAPI app so that it would not block requests from my UI app.

version: '3.4'

services:
  ui:
    container_name: ui_container
    image: ${DOCKER_REGISTRY-}ui
    build:
      context: .
      dockerfile: UI/Dockerfile
    ports:
      - "49254:443"
  api:
    container_name: api_container
    image: ${DOCKER_REGISTRY-}api
    build:
      context: .
      dockerfile: API/Dockerfile
    ports:
      - "49255:443"

So, to hit my API endpoint I had to ajax to https://localhost:49255/WeatherForecast.

To see all my code, view my GitHub repository.

Obviously I would need to adjust the hostnames if I were to move this app into production.

If anyone knows of a better/cleaner solution, please make suggestions.

Joshua Abbott
  • 342
  • 1
  • 3
  • 11
1

From what I understand from this you have 2 ASP.NET Projects that you want to have communicate with each other. That's simple enough to do and your on the right track. On your docker compose you want to create a network for the 2 Containers. If you want to open up the ports both the API & UI (Port: 80) you could map them, or if you only want to access the UI and don't care about hitting the API from anything other then the UI then just open up port 80 to your host network (or any other port of your choice). Go ahead and create a network to experiment with. docker network create MyNetwork. After you have done edit your docker-compose.yml (Note you can create the network in your docker compose, I'm just describing this way so you understand what ultimately is happening behind the scene)

version: '3.4'

services:
  ui:
    # Always a good practice to set container names.
    container_name: asp_net_ui
    image: ${DOCKER_REGISTRY-}ui
    build:
      context: .
      dockerfile: UI/Dockerfile
    ports:
      - "80:80"

  api:
    container_name: asp_net_api
    image: ${DOCKER_REGISTRY-}api
    build:
      context: .
      dockerfile: API/Dockerfile
      
# As I said before this is refrenceing a pre excisting network but you could
# Create a new network here aswell. I tend to use ths approach as I have multiple
# Diffrent stacks running in a network, e.g. Caddy, Jenkins, Dev Staging Env.
networks:
  default:
    external:
      name: MyNetwork

After firing this up to answer your question about What is the URL that will hit my endpoint?
You can use DNS Resolution. Simply provide the containers name.

For Container to Container Communication:
To hit your API: http://asp_net_api/{controller}
To hit your UI: http://asp_net_ui/{view}

To Access your UI from your browser: http://localhost/{view}

I hope this has helped in one way or another, If you have any more questions feel free to fire away or reference the Official Networking Documentation here

  • 2
    Thank you for this super clear answer. I had just 5 minutes this morning to try it out and couldn't get it working, but I'll get back on it when have time to prototype more. – Joshua Abbott Jan 12 '21 at 15:31
  • 1
    bear with me because I don't know much about networking, but I'm not getting this to work. So I've used ```docker network create TestNetwork``` to create a network. I have then added ```networks: default: external: name: TestNetwork``` at the end of my docker-compose.yml so I know they are on the same network. Then I have added ```ports: - "80:80"``` to only my ui service. After that, calling ```http://asp_net_api/{controller}``` or ```http://asp_net_api:80/{controller}``` still doesn't hit my method in the API project. I have also tried moving the ports to the api service. – Joshua Abbott Jan 13 '21 at 15:32
  • upload it to github and send me through the link and I will have a look and push up the changes. – Jeremy Ruffell Jan 13 '21 at 22:48
  • 1
    I sent you a collaboration invite on GitHub. – Joshua Abbott Jan 13 '21 at 23:18