Multiple virtual hosts with SSL on one machine

4 min read

In this article, we’ll set up multiple websites with SSL on one machine using Docker, Docker Compose, nginx, and Let’s Encrypt.

ClientHTTP / HTTPS request

Docker containers · shared network

Let’s Encrypt companionGenerates certificates
Shared certificates
Nginx proxyRoutes requests by hostname
Nginx virtual hostwww.example.com
Nginx virtual hostwww.anotherone.com
Additional websites…
One proxy routes client requests to the matching website container.

Prerequisites

You’ll need some experience with Ubuntu and a basic understanding of Docker, with Docker and Docker Compose installed. If you need help getting them ready, see the Docker installation guide and the Docker Compose installation guide.

Nginx proxy

To host several websites on one machine, we need a proxy that sends each request to the right nginx container. We’ll use the jwilder/nginx-proxy Docker image, which configures the proxy as we launch website containers.

Let’s Encrypt companion

For automatic certificate management, we’ll use the jrcs/letsencrypt-nginx-proxy-companion image. It watches for new containers and works with the nginx proxy to set up SSL certificates for them.

Main Docker Compose file

Let’s start with the main Docker Compose file. It will launch the nginx proxy and Let’s Encrypt companion containers. Create a folder called proxy, then add a docker-compose.yml file with the following content:

version: '3' # Version of the Docker Compose file format
services:
    nginx-proxy:
        image: jwilder/nginx-proxy:alpine
        restart: "always" # Always restart the container
        ports:
         - "80:80" # Port mappings in host:container format
         - "443:443"
        networks:
         - nginx-proxy # Name of the network these two containers will share
        labels:
         - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy" # Label used by the Let’s Encrypt companion
        volumes: # Volumes for proxy configuration and certificates from the Let’s Encrypt companion
         - /var/run/docker.sock:/tmp/docker.sock:ro
         - "nginx-conf:/etc/nginx/conf.d"
         - "nginx-vhost:/etc/nginx/vhost.d"
         - "html:/usr/share/nginx/html"
         - "certs:/etc/nginx/certs:ro"
    letsencrypt-nginx-proxy-companion:
        image: jrcs/letsencrypt-nginx-proxy-companion
        restart: always
        container_name: letsencrypt-nginx-proxy-companion
        volumes:
         - "/var/run/docker.sock:/var/run/docker.sock:ro"
         - "nginx-conf:/etc/nginx/conf.d"
         - "nginx-vhost:/etc/nginx/vhost.d"
         - "html:/usr/share/nginx/html"
         - "certs:/etc/nginx/certs:rw"
        depends_on: # Start the nginx proxy container first
            - nginx-proxy
networks:
    nginx-proxy: # The shared network used by the containers
volumes: # Shared volumes that persist on the Docker host
    nginx-conf:
    nginx-vhost:
    html:
    certs:

Now run:

docker-compose up -d
Docker Compose will look for docker-compose.yml in the current folder and start the services defined there: in this case, the nginx proxy and Let’s Encrypt companion.

The -d flag runs the containers in the background, also called detached mode.

To check the containers’ status, run:

docker ps
You should see two containers with the status Up.

Nginx virtual host

Next, we’ll launch a website container. The proxy will route requests to it.

Create a folder called example.com, replacing that name with your own domain. Add a new docker-compose.yml file, and put your website’s files in a subfolder called www. Create another subfolder called nginx, containing Dockerfile and default.conf.

Your example.com folder should look like this:

Dockerfile:

FROM nginx:alpine
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf # Copy the nginx configuration file
COPY ./www/ /usr/share/nginx/html/ # Copy the website files

default.conf:

server {
	listen 80 default_server;
	listen 443 ssl;

	server_name example.com;

	root /usr/share/nginx/html/;
	index index.html;

	location / {
		try_files $uri $uri/ =404;
	}
}

docker-compose.yml:

version: '3'
services:
    nginx:
        container_name: example-nginx
        image: example-nginx
        restart: always
        build:
            context: ./
            dockerfile: ./nginx/Dockerfile
        environment:
         - VIRTUAL_HOST=example.com # Domain used by the nginx proxy
         - LETSENCRYPT_HOST=example.com # Domain used by the Let’s Encrypt companion
         - [email protected]
        expose:
         - "80" # Expose the HTTP port
         - "443" # and the HTTPS port
        networks:
         - nginx-proxy # Connect to the nginx-proxy network defined below
networks:
    nginx-proxy:
        external:
            name: proxy_nginx-proxy # Use the network created by the main Compose file. Run `docker network ls` to check its generated name.
Remember to replace example.com with your own domain in all the files.

Everything is ready. Build and start the container:

docker-compose up -d

You should now have three containers running: the nginx proxy to route requests, the Let’s Encrypt companion to manage certificates, and the website container to serve your content.

That’s it!

Your website should now be available over HTTPS!
To add another website, repeat the steps in the Nginx virtual host section.

Rebuilding a container image

If you change the website’s files and need to rebuild its image, run:

docker-compose build
This rebuilds the service images defined with a build configuration in the current folder’s docker-compose.yml.

What if something isn’t working?

If a container won’t start, try running it without -d. You’ll see its output directly in the terminal, including stdout and stderr.

To read a container’s logs, use:

docker logs CONTAINER_ID

You can find the CONTAINER_ID in the output of:

docker ps

SNI

SNI (Server Name Indication) lets one host serve different domains with different certificates. You can read more on Wikipedia and check browser support on Can I use.

The example on GitHub

You can find the example project on GitHub. Have fun!