Multi domain Nginx with automatic Letsencrypt certificate

Rhuan Silva
Former Tech. Lead at XNV

What was the problem? I had a SWARM cluster with just one node and a Nginx server installed bare metal with all the domain config files in a folder. In this scenario, every new application I had to deploy on the cluster, map a different port and tell baremetal Nginx/CertBot to issue the certificate and proxy the requests.

Issues I neede to solve

  • Docker swarm single node must be converted do multi node
  • New apps should have Lets Encrypt certificate issued automatically
  • Needed to redirect HTTP->HTTPS
  • Every container should have one port
  • If a container is deployed on other node, the port would be mapped on the other host so Nginx in cluster head won´t find the service

As is diagram

As is diagram

To be diagram

To be diagram

Solution: use the nginx-proxy + acme-companion deployment


  • I used /opt/docker as my volumes mount.
  • I used docker-compose.servicename.yaml as the docker-compose file name (I don´t want one folder per app, just like that :))
  • This article does not cover database or applications (WordPress, Odoo, Miniflux) deployment so, database and other stuff must work first. I advise you to map host port before deploying to ensure the applications is working without certificate

Docker-compose files


version: "3"services:  teampass:    image: wordpress    environment:      VIRTUAL_HOST:      LETSENCRYPT_HOST:      LETSENCRYPT_EMAIL:    networks:      - common_backend    volumes:      - /opt/docker/miniflux:/var/www/htmlnetworks:  common_backend:    external: true


version: "3"services:  teampass:    image: miniflux    environment:      VIRTUAL_HOST:      LETSENCRYPT_HOST:      LETSENCRYPT_EMAIL:    networks:      - common_backend    volumes:      - /opt/docker/miniflux:/var/www/htmlnetworks:  common_backend:    external: true


version: "3"services:  teampass:    image: odoo    environment:      VIRTUAL_HOST:      LETSENCRYPT_HOST:      LETSENCRYPT_EMAIL:      VIRTUAL_PORT: 8069    networks:      - common_backend    volumes:      - /opt/docker/wordpress:/var/www/htmlnetworks:  common_backend:    external: true


version: '3.3'services:    nginx-proxy:        image: nginxproxy/nginx-proxy        ports:            - '80:80'            - '443:443'        deploy:          placement:            constraints:              - node.hostname == megazord        volumes:            - 'certs:/etc/nginx/certs:rw'            - 'vhost:/etc/nginx/vhost.d:rw'            - 'html:/usr/share/nginx/html'            - '/var/run/docker.sock:/tmp/docker.sock:ro'        labels:          - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"        networks:        - common_backend    acme-companion:        image: nginxproxy/acme-companion                volumes:            - '/var/run/docker.sock:/var/run/docker.sock:ro'            - 'acme:/etc/'            - 'certs:/etc/nginx/certs:rw'            - 'vhost:/etc/nginx/vhost.d:rw'            - 'html:/usr/share/nginx/html'        deploy:          placement:            constraints:              - node.hostname == megazord        environment:            -        networks:        - common_backendvolumes:  certs:  vhost:  html:  acme:networks:  backend:    driver: overlay

Some things to pay attention:

Have a SWARM cluster already set. If you don´t, I’ll write a post with the step-by-step for this sometime.

Network must be the same for Acme Companion, Nginx Proxy and your apps

Once the nginx-proxy will call your service by the name (e.g. http://teampass) in order to respond your client, you must determine these three variables

The “common” name is used here to say that all services that is common to all stacks, is declared on  docker-compose.common.yaml file (databases, redis and other services too). Deploy this first to ensure creation of  “common_backend” network

      LETSENCRYPT_HOST: #this is the same name you will configure in our DNS      LETSENCRYPT_EMAIL: #some emmail      VIRTUAL_PORT: 8080 #the port that the container exposes inside the SWARM cluster. If the port is 80, you can hide this variable

The volumes in nginx stack will be shared between nginx-proxy and nginx-acme_companion. Because of this, you should declare the volumes in the top level of yaml

That said, lets deploy it!

docker stack deploy -c docker-compose.common.yaml commondocker stack deploy -c docker-compose.wordpress.yaml wordpressdocker stack deploy -c docker-compose.miniflux.yaml minifluxdocker stack deploy -c docker-compose.odoo.yaml odoo


News, lessons, and content from our companies and projects.

Stay In The Loop!

Receive updates and news about XNV and our child companies. Don't worry, we don't SPAM. Ever.