Deploy and automatically provision SSL certs on a Node.js server with Traefik and Docker.

In this tutorial, we would learn to deploy Node.js servers with docker on a Linux-based VPS and automatically provision SSL certificates with Traefik.

Prerequisites:

  • Basic familiarity with Node.js, docker and docker-compose.
  • A virtual private server with a public IP address from any cloud service provider of your choice.
  • A domain or subdomain and a corresponding DNS record pointing the public IP address of your VPS.

Introduction

Traefik is an open-source, cloud-native reverse proxy. A reverse proxy essentially sits in front your servers and handles incoming clients request. So instead your client request directly going to your Node.js server, the request first goes through Traefik and Traefik then forwards it to your server. This allows us do things like SSL encryption, canary deploys, load balancing among others.
traefik demo image

Now lets get started!

Spin Up a Linux VPS

This could be an Amazon EC2 instance, A digital ocean droplet, Linode VMs, etc or even an on-prem Linux machine with publicly accessible IP address. For this demo, I am using an ubuntu digital ocean droplet.

Install docker and docker compose.

This tutorial focuses on the deployment phase. You can read the docker docs on how to install docker and docker compose for your respective platforms.

Firewall restrictions

Depending your VPS and setup ensure that both port 80 and port 443 are accessible from outside. This could mean adjusting the inbound rules of your security group in your VPC on AWS or opening the ports on ufw.

DNS records

If you haven't done so already, create a DNS record for your domain or sub-domain and point it to the public IP address of your VPS. You can confirm the DNS propagation by pinging your domain and seeing that it resolves to the IP address of your VPS. If you use a DNS provider like cloudfare that supports proxying, you might want to turn off this feature while testing.

ping mydomian.com // should resolve to your VPS IP address

Node.js Server

In this example, I will be demonstrating using a simple fastify server.

//server.js file 

const fastify = require("fastify")({ logger: true });
fastify.get("/", async (request, reply) => {
  return { message: "Hello world! I'm using fastify" };
});
const start = async () => {
  try {
    await fastify.listen(3000, "0.0.0.0");
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
start();

Dockerise Node.js server

We containerise our Node.js server with docker using the Dockerfile below.

FROM node:12-alpine
RUN mkdir home/node-traefik
WORKDIR /home/node-traefik
COPY . .
RUN npm install
EXPOSE 3000
CMD [ "node", "server.js" ]

N/B: This is a basic example on how to dockerise a Node.js App. For production use cases, you should probably read the Node.js and Docker best practices guide here.

Docker Compose

Now we create a docker-compose file and reference our Dockerfile. At this stage we can start our Node.js server with the docker-compose up command.

services:
  node-server:
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "3000:3000"

Configuring Traefik

To introduce Traefik into our flow, we create a new service for Traefik in our docker-compose file.

services:
  reverse-proxy:
    image: traefik:v2.4
    container_name: "traefik"
    command:
      - "--api.insecure=true"
      - "--api.dashboard=true"
      - "--api.debug=true"
      - "--providers.docker=true"
      - "--log.LEVEL=DEBUG"
      - "--entryPoints.web.address=:80"
      - "--entryPoints.websecure.address=:443"
      - "--providers.docker.exposedbydefault=false"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
      - "[email protected]"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "443:443"
      - "80:80"
      - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

Traefik has the concept static and dynamic configurations. Static configuration are needed by Traefik at startup time and if changes are made to the static configurations, Traefik has to be restarted for these changes to take effect. When using Traefik with docker-compose, we define static configurations as commands in the docker-compose file.

Lets go through each command in the static configuration individually.

  • - "--providers.docker=true" tells traefik that docker is our key infrastructure components and thus traefik queries the docker API for the relevant information it needs.
  • --api.insecure enables the traefik dashboard in insecure mode. For production uses cases, you want to use basic authentication and TLS on the dashboard.
  • - "--providers.docker.exposedbydefault=false" tells traefik not to expose a service unless being explicitly to do so.

  • The mounted volume with- "/var/run/docker.sock:/var/run/docker.sock:ro" allows Traefik to communicate with docker.

  • The - "--entryPoints.web.address=:80" and - "--entryPoints.websecure.address=:443" line declare a network and corresponding port entry points into Traefik.

  • The "[email protected] creates a certificate resolver named myresolver. The certificate resolver is responsible for generating, renewing and disposing certificates.

  • - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" Tells our certificate resolver to save our certificates in acme.json file in the letsencrypt volume.

  • - "--certificatesresolvers.myresolver.acme.httpchallenge=true" Tells the certificate resolver to use the HTTP challenge.

At the this stage, if we start up our containers, the Traefik dashboard will be accessible on port 8080http://<IP>:8080.
sample traefik dashobaord

Our Node.js server services has not been linked to Traefik yet. This is where the concept of dynamic configuration comes in. Unlike static configurations, dynamic configurations can be updated after Traefik has started. Traefik watches for changes in the dynamic configurations and applies it with no need to restart Traefik. When using Traefik with docker, we add dynamic configurations by using labels. Traefik reads these meta-data and applies it to the respective service.

node-server:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: node-server
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.node-server.rule=Host(`play.paularah.com`)"
      - "traefik.http.routers.node-server.entrypoints=websecure"
      - "traefik.http.routers.node-server.tls.certresolver=myresolver"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.redirs.entrypoints=web"
      - "traefik.http.routers.redirs.middlewares=redirect-to-https"
  • Since we configured Traefik not to expose services except being explictly told to do so, The - "traefik.enable=true" label now exposes our Node.js server container to Traefik.

  • - "traefik.http.routers.node-server.rule=Host(play.paularah.com)" creates a router that routes network request from the domian play.paularah.com to the Node.js server container.

  • - "traefik.http.routers.node-server.tls.certresolver=myresolver"
    tells the router to use the certificate resolver we created earlier.

  • - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" creates a middleware to force-redirect HTTP network request to HTTPS.

The next three labels creates a router that matches every request to the host on port 80 and then uses the redirect to https middleware we created previously.

Restarting the containers now and voila! our Node.js server is now available from play.paularah.com, uses SSL and force redirects to HTTPS.

Summary

Traefik makes deploying HTTP servers with docker really easy. We can deploy multiple projects on the same host, by simply adding more services to our docker-compose file. One major advantage of this setup of is having all our configurations in one place and in a single command docker-compose up everything is up and running. This also makes our entire setup easily reproducible and allowing us move a project easily from one cloud service provider to another.

The source code for this blogpost can be found at https://github.com/paularah/node-traefik

16