6

I am recently decided to migrate my dev environment from native mac to docker for mac, and I would like to have multiple projects expose the same port 80, so that I can simply type http://app1.dev/ and http://app2.dev/ in the browser without remembering dozens of port numbers.

I don't have to do anything on native environment to achieve this. But since now nginx runs separately in each container they are conflicted on port exposing. I also know that I can use an external link to an external container but I don't want to tear apart my docker-compose.yml file, I just want everything in one piece.

docker-compose.yml in ~/demo1/

version: '3'
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
# ... 

docker-compose.yml in ~/demo2/

version: '3'
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
# ...

When I issue command docker-compose up -d in demo2 I got:

Creating network "demo2_default" with the default driver
Creating demo2_web_1 ... error

ERROR: for demo2_web_1  Cannot start service web: driver failed 
programming external connectivity on endpoint demo2_web_1 
(cbfebd1550e944ae468a1118eb07574029a6109409dd34799bfdaf72cdeb3d35): 
Bind for 0.0.0.0:80 failed: port is already allocated

ERROR: for web  Cannot start service web: driver failed programming 
external connectivity on endpoint demo2_web_1 
(cbfebd1550e944ae468a1118eb07574029a6109409dd34799bfdaf72cdeb3d35): 
Bind for 0.0.0.0:80 failed: port is already allocated
ERROR: Encountered errors while bringing up the project.

Is there any way to make them share them same port or maybe remap ports from host to container without using extra commands to create external containers? Or is there a way to create external containers within the docker-compose.yml file?

chouyangv3
  • 467
  • 5
  • 12

1 Answers1

5

You cannot map multiple container ports to the same host port. Whatever container that comes up first will get binded to port 80 and the second container will throw port already in use if you try binding the same port.

To Address this issue you can run another dummy Nginx which simply does proxy_pass to demo1 and demo2. In this case, http://app will be parent Nginx and /dev1 will proxy_pass to demo1 and /dev2 will proxy_pass to demo2.

Here, You just need to bind parent Nginx's port to host. You don't need to bind ports of child Nginx if you connect all these to the same network and use docker service discovery. If you follow these then you will hit another problem i.e: Nginx will cache IP of containers resolved using service discovery and will use that IP to hit the containers always. Once the child container is restarted there is a possibility that the child's IP address might get changed, So parent Nginx throws 502. To solve this you have to restart parent Nginx each time whenever you restart demo1 or demo2. To solve this you have to use resolver as 127.0.0.11 with validity in parent Nginx. So each time Nginx will try resolving the IP address post last resolution according to validity.

I've added dummy config files summarising all the above points.

Parent Nginx compose:

version: '3'
services:
  parent:
    image: nginx:alpine
    volume:
         - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
         - 80:80
networks:
  default:
    external:
      name: dev

Parent Nginx config (./nginx.conf):

server {
    listen 80;

    resolver 127.0.0.11 valid=5s; #this is local docker DNS and the internal IP getting resolved will be valid only for 5 seconds.

    location /app/dev1 {
        proxy_pass http://dev1:80;
    }

    location /app/dev2 {
        proxy_pass http://dev2:80;
    }
}

docker-compose.yml in ~/demo1/

version: '3'
services:
  web:
    image: nginx:alpine
  networks:
    default:
       aliases:
          - dev1
networks:
  default:
    external:
      name: dev

docker-compose.yml in ~/demo2/

version: '3'
services:
  web:
    image: nginx:alpine
  networks:
    default:
       aliases:
          - dev2
networks:
  default:
    external:
      name: dev

Now you can use demo1 by hitting URL http://app/dev1 and demo2 by using http://app/dev2.

References:

  1. NGINX Reverse Proxy
  2. NGINX proxy_pass
  3. NGINX resolver
  4. Necessity of docker DNS
  5. Docker container networking
Mani
  • 5,401
  • 1
  • 30
  • 51
  • That's a great idea! I haven't thought about that! Not a perfect solution but it works for me! Thank you. – chouyangv3 Dec 21 '18 at 16:45
  • I think this is the only solution according to your requirement as per my knowledge. Anyways hope that helps. – Mani Dec 21 '18 at 18:05
  • Could you explain the line `resolver 127.0.0.11 valid=5s` better? – xjcl Jun 19 '20 at 17:21
  • `127.0.0.11` is docker dns. Since we use docker's service discovery. Nginx caches's resolved ip. Sometimes the resp docker container might get restarted in such cases nginx will still resolve to old ip resulting in 502. Hence we invalidate nginx's cached ip once in 5 seconds. Such that in case a container restarts and gets new ip nginx will resolve to it within 5 seconds. – Mani Jun 19 '20 at 17:26