Wednesday, September 18, 2024

Use nginx running in Docker container to serve .NET sites running on Kestrel both in host and Docker containers

In one of my previous articles I made brief introduction to Docker – containerization technology which gives more control over infrastructure. Suppose that we have number of .NET sites running on Kestrel in Linux and published via nginx to the internet like shown on the following schema (ports numbers are only for example here):

And we want to move all our infrastructure to Docker (backend, database, nginx itself, etc) so it will look like this:

On practice however this switch will take time and at some time period we will have both sites running still in the Linux host and sites running in Docker containers, and we will need to serve both types:

Since only one app may listen to 80 port we need to decide whether we want to keep nginx service running on the host or move nginx to Docker and configure it to serve both sites from the host and sites from Docker containers. In this article I will describe this last option.

Since nginx is running in Docker there won’t be many problems with hosting sites running also in Docker. Just run these sites and nginx container in the same network:

#docker-compose-nginx.yml
name: myservice
services:
  nginx:
    image: nginx:latest
    ports:
      - 80:80
    …
    networks:
      - mynetwork

and nginx will be able to resolve containers by names. I.e. if site’s container name is mysite-web-1 you may specify nginx.conf like this:

server {
	listen 80;
	listen [::]:80;
	server_name mysite.ru www. mysite.ru;
	location / {
		proxy_pass http://mysite-web-1:80;
		…
	}
}

However with sites running in host it is not that straightforward. Since nginx is running in Docker container which has own IP address we need to instruct it to which IP it should forward requests if they came for sites running on the host.

We can do that by adding special extra host host.docker.internal to nginx docker compose:

name: myservice
services:
  nginx:
    image: nginx:latest
    ports:
      - 80:80
    restart: always
    networks:
      - mynetwork
    extra_hosts:
      - host.docker.internal:host-gateway

If we will check /etc/hosts file inside nginx container we will see that host.docker.internal points to 172.17.0.1 IP address which is default IP used by Docker for host:

docker exec -it myservice-nginx-1 sh
more /etc/hosts
…
host.docker.internal 172.17.0.1

Next step is to instruct nginx to forward request to the host if it came for site running there. For doing that we need to modify nginx.conf and specify host.docker.internal with appropriate port in proxy_pass property:

server {
	listen 80;
	listen [::]:80;
	server_name mysite.ru www.mysite.ru;
    	location / {
		proxy_pass http://host.docker.internal:5000;
		…
    	}
}

However that is still not enough. If your .NET site is running on Kestrel you most probably configured to run it as a daemon via the following commands:

systemctl enable mysite.ru.service
systemctl start mysite.ru.service

where mysite.ru.service file contains the following line:

Environment=ASPNETCORE_URLS=http://localhost:5000

With our current configuration if we will open shell inside our nginx container and try to reach our site via curl we will get Connection refused:

docker exec -it myservice-nginx-1 sh
curl -X GET http://host.docker.internal:5000
Connection refused

The problem here is that Kestrel is currently listening only for localhost IP (127.0.0.1). We can check it on the host using the following command:

netstat -tulpn | grep 5000
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      22496/dotnet
tcp6       0      0 ::1:5000                :::*                    LISTEN      22496/dotnet

but request from nginx container goes to 172.17.0.1. To solve that we need to modify /etc/systemd/system/mysite.ru.service and add http://172.17.0.1:5000 to ASPNETCORE_URLS after semicolon:

Environment=ASPNETCORE_URLS=http://localhost:5000;http://172.17.0.1:5000

and reload the service:

systemctl stop mysite.ru.service
systemctl daemon-reload
systemctl start mysite.ru.service
systemctl status mysite.ru.service

After that check that it listen 5000 port also on 172.17.0.1:

netstat -tulpn | grep 5000
tcp        0      0 172.17.0.1:5000         0.0.0.0:*               LISTEN      22496/dotnet
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      22496/dotnet
tcp6       0      0 ::1:5000  

And now if we go inside container shell and try to reach the site connection should be successful:

docker exec -it myservice-nginx-1 sh
curl -X GET http://host.docker.internal:5000
Connection successful

which means that nginx is now able to serve sites running both in containers and on the host itself.