How to host multiple servers with different sub domains inside one DigitalOcean droplet with SSL (auto-renew)
If you’re playing around with multiple technology stacks or extending open-source applications and you don’t mind having to shift back and forth between different languages and frameworks, you might find containerizing your applications useful.
I have found it easier to use Docker to containerize my pet projects for obvious reasons Docker usually poses. But then comes the challenge of making them available outside my workstation. DigitalOcean is a good place to host containerized applications (but you probably already know this since you’re here), and you can do so cheaply if you use a small droplet with about 2GB of memory. That’s enough for personal or small-scale usage of 2–3 applications.
Assigning domains to droplet
You can assign multiple top-level domains or subdomains to a single droplet in DigitalOcean for no additional cost. This can be easily achieved from your droplet’s control panel, as seen below.
and then it is as easy as typing in the domain name like below (assuming the domain is already pointed towards your DigitalOcean droplet from your domain’s DNS management portal).
I will not go into the well-known steps of pointing your domain from your registrar to DigitalOcean, but it is as usual and as documented here.
Multiple containers
In this example I will consider two web-apps, one on NodeJs and one on Java-Spark (Jetty server) in each container. Both containers are deployed on Docker in a single droplet with the following ports exposed for simplicity.
NodeJs:
container: node_conthost port: 4000
internal port: 80 //http only - avoiding https
Java-Spark:
container: java_conthost port: 8000
internal port: 80 //http only - avoiding https
I will not be configuring individual servers with SSL thus exposing them only with http. This is because it is easier to apply SSL to the whole configuration once which will be easier to maintain and will avoid double encryption.
If everything went right up to now, you should be able to reach your web applications and use them through following URLs.
Note: For our configuration, we will use a docker network. Therefore we do not need to be concerned about the port mapping between server droplets and the host. What is important is the port your server uses on the container. At the end of the day, we will use the host ports only with a another container, not our development server containers.
http://<droplet’s IP>:4000http://<droplet’s IP>:8000
Configuration Overview
As I said in the beginning, a small droplet of 2GB memory should be enough to hold up to 2 or 3 containers and be functional, as long as you’re not planning for a production run. Either way, deciding on a droplet is your task and has no bearing on this set-up. Only emphasis is that this configuration will work even in such a small droplet.
We will use NginX in our third container and it will handle the reverse-proxy/ virtual host situation on behalf of our droplet. You can install Nginx in your droplet without Dockerizing it, of course. I prefer to containerize NginX as well so I can move the whole set-up without depending on the droplet too much if I have to. The configuration chapter will cover details of setting up the NginX container.
Our final configuration shall look like below.
Here, I’m assuming the droplet IP will be 192.168.10.55 for demonstration purposes, but you get the idea. For the NginX container to talk to the other containers by their container name rather than IP/domain name, they all will need to be on the same docker network. Let’s get one set-up.
Creating a Docker Network
Create a Docker Network with any name, in this case I will call it as below:
docker network name: our-network
docker network create -d bridge our-network
Make sure all three containers are connected to this network.
Our NginX container need to speak to the other containers. This is why we need a network and all the containers added to it. It doesn’t matter what you decide to call it or if it is an external network or if it is an automatic network created by docker-compose.
You can achieve this with docker-compose like so:
version: ‘3.4’
services:
example_service:
container_name: example_container
...
networks:
— our-network #Important!
volumes:
...
ports:
...
networks:
our-network: #Important!
external: true #Important!
or if you prefer scripts, like so:
docker run --network=our-network -itd --name=example_container docker-image
That takes care of the communication barrier. We’re ready to set up NginX and configure it.
One config to bring them all — and in the droplet bind them
Configuration will have two parts. Initial configuration is part of the NginX container setup itself. Second will be the custom configuration of NginX.
NginX container
Our NginX container will be spun -up using docker-compose. You can use an alpine based NginX image. But we need more things from NginX, that is, we need SSL for each and every sub container and we want the SSL auto-renewed without administration. It takes more steps to install the necessary packages and scripts to get CERTBOT working on alpine. For the sake of convenience I will use nginx:stable image. But if you have resource constraints on your droplet, I advise using alpine and figure out the CERTBOT and CRON steps manually (at least for the first time).
For the NginX container, we will have a docker-compose.yml file, a Dockerfile inside a directory called ‘nginx’ and then another directory structure within that for nginx configuration.
The directory structure to setup the NginX container must be like so:
nginx/
├─ Dockerfile
├─ config/
│ ├─ conf.d/
│ │ ├─ prod/
│ │ │ ├─ example.conf
docker-compose.yml
- Note that the directory (./nginx/config/conf.d/prod ) is for the purpose of storing our configuration file for NginX. This will be bound to a volume so we can change the configuration on the fly later on. Easy for maintenance. Feel free to play around with it. Other files/directories are necessary.
note lines marked with ##Change this
The Dockerfile will look like so:
FROM nginx:stableARG CERTBOT_EMAIL=youremail@domain.com ##Change thisARG DOMAIN_LISTRUN apt-get update \&& apt-get install -y cron certbot python-certbot-nginx bash wget \&& certbot certonly --standalone --agree-tos -m "${CERTBOT_EMAIL}" -n -d ${DOMAIN_LIST} \&& rm -rf /var/lib/apt/lists/* \&& echo "PATH=$PATH" > /etc/cron.d/certbot-renew \&& echo "@monthly certbot renew --nginx >> /var/log/cron.log 2>&1" >>/etc/cron.d/certbot-renew \&& crontab /etc/cron.d/certbot-renewVOLUME /etc/letsencryptCMD [ "sh", "-c", "cron && nginx -g 'daemon off;'" ]
Change the CERTBOT_EMAIL to yours. This receives emails if certificate renew fails or notifications about certificate expiration.
The docker-compose.yml file will look like so:
version: '3.4'services:nginx: container_name: nginx build: context: ./nginx network: host args: - CERTBOT_EMAIL=youremail@domain.com #replace with your own email - DOMAIN_LIST=one.domain.com,two.domain.com,three.domain.com #replace with your own domains restart: always networks: - our-network #The docker network we created volumes: - ./nginx/config/conf.d/prod:/etc/nginx/conf.d - letsencrypt:/etc/letsencrypt ports: - "80:80" - "443:443"networks: our-network: external: truevolumes: letsencrypt: null
Note:
- Replace CERTBOT_EMAIL with your email address
- Replace DOMAIN_LIST with all your domains and sub domains that NginX will manage. It doesn’t matter if you include them in your configuration or not, this is purely for getting SSL certificates from Let’s Encrypt.
- Do not add wildcard domain names or they will not auto-renew. CERTBOT cannot verify wildcard domains without DNS records. Add each and every domain name here and CERTBOT will auto-renew them via a CRON job.
- We are creating two volumes here (or one, depends on how you look at it). One volume is bound to the directory with our *.conf file, the other is to store Let’s Encrypt issued certificates if you wanted access to them.
- Note that NginX container is mapped to port 80 (for http) and 443 (for https) of the host, in this case the droplet with Ubuntu. This means NginX container will handle all http and https traffic coming towards the droplet’s IP.
- The NginX container is also part of the Docker network created, ‘our-network’.
- NginX container will auto restart if it ever shuts down.
Finally, we come to the NginX configuration. (./nginx/config/conf.d/prod/example.conf)
A sample configuration:
#Srimal Fernando#traffic on port 80 will all go to ssl. create ssl server blocks for new subdomains.#docker network: our-network needs to be in place connecting nginx and other containers#proxy_pass to http://<container_name>:<container_port>#/etc/letsencrypt/live/one.domain.com/fullchain.pem#/etc/letsencrypt/live/one.domain.com/privkey.pemmap $http_x_forwarded_proto $redirect_scheme { default $scheme; https https;}# Permanently redirect all traffic on http to httpsserver {listen 80 default_server;server_name _;return 301 https://$host$request_uri;
}
server{listen 443 ssl;server_name one.domain.com;ssl_certificate /etc/letsencrypt/live/one.domain.com/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/one.domain.com/privkey.pem;location / {proxy_set_header Connection "";proxy_pass http://java_cont:8000;}}
server {listen 443 ssl;server_name two.domain.com;ssl_certificate /etc/letsencrypt/live/one.domain.com/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/one.domain.com/privkey.pem;proxy_ssl_name two.domain.com;location / {proxy_set_header Connection "";proxy_pass http://node_cont:4000;}}server {listen 443 ssl;server_name three.domain.com;ssl_certificate /etc/letsencrypt/live/one.domain.com/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/one.domain.com/privkey.pem;proxy_ssl_name three.domain.com;location / {proxy_set_header Connection "";proxy_pass http://another_cont:9001;}}
Note:
- If a user hits any domain under our droplet without SSL, the first block will redirect them down to the blocks below. This will ensure you don’t get an NginX error page if a visitor doesn’t specify https:// when visiting your applications. This includes browser visits as well as GET, POST or any other HTTP requests, even as API calls.
- NginX container has volume bound to “/etc/letsencrypt/” directory in the droplet’s file system. When spinning up the Nginx container, it will automatically fetch the certificates and store it in this directory. Even though we obtain certificates for multiple domains, they will bundled together and will get renamed as only one of the domains, this is ok. Check the directory for the actual name of the file when configuring Nginx.
- proxy_ssl_name is the domain to point to.
- proxy_pass should be http and the hostname should be the name of the container, and the container’s port, not the port of the host it’s mapped to.
- Make sure to disable or not configure SSL within the servers in your containers other than Nginx. Nginx will encrypt the traffic from the outermost layer.
And that concludes the configuration.
To wrap things up, you can add some monitoring set-up on the droplet to make sure you have enough memory and CPU to not run into an app that fails because of not enough resources. I’ll link an article here if I write one.
This set up should self-sustain without any hiccup, I’ve had a slightly bigger setup open to traffic for over 6 months with no maintenance needed.
If you need to change the configuration, simply edit the .conf file and issue a command to the NginX container to reload the configuration.
docker exec -it <nginx container name> nginx -s reload
If you need to add new domains, simply edit the docker-compose file and stop and start the nginx container again, new certificates will be issued.
That is it. Enjoy building cloud applications without spending unnecessarily.