I Switched from NPM to Traefik

I really like NPM. But Traefik allows me to do the same, without going through a web UI. So I switched, and this is how I've done it.

I Switched from NPM to Traefik
Photo by Robin Pierre / Unsplash

I really like Nginx Proxy Manager. It's easy to use, quick to set up and does everything I'm looking for in a reverse proxy. But Traefik always intrigued me. It has the same functionality as NPM, but you don't have to interact with an interface. It loads the config based on the labels on your docker containers. In the short run, it would take longer to implement. However in the long run, it would save me from having to define every new instance in the web interface and use tags instead. So I switched.

Setting up Traefik

I'll show you my compose file and then I'm going to try and explain it step by step.

version: "3"

networks:
  default:
    external:
      name: npm_network

services:
  traefik-app:
    image: traefik:latest
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${DATADIR}/app/letsencrypt:/letsencrypt
    command: 
      - --api.insecure=true
      - --providers.docker
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.forwardedHeaders.trustedIPs=172.26.0.0/16,172.80.0.0/24
      - --entrypoints.websecure.address=:443
      - --entrypoints.websecure.forwardedHeaders.trustedIPs=172.26.0.0/16,172.80.0.0/24
      - --entrypoints.websecure.http.tls.domains[0].main=${DOMAIN}
      - --entrypoints.websecure.http.tls.domains[0].sans=*.${DOMAIN}
      - --entrypoints.websecure.http.tls.certresolver=myresolver
      - --certificatesresolvers.myresolver.acme.dnschallenge=true
      - --certificatesresolvers.myresolver.acme.dnschallenge.provider=godaddy
      - --certificatesresolvers.myresolver.acme.email=${MAIL_ADR}
      - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
    environment:
      GODADDY_API_KEY: ${GODADDY_API_KEY}
      GODADDY_API_SECRET: ${GODADDY_API_SECRET}
      GODADDY_POLLING_INTERVAL: 10
      GODADDY_PROPAGATION_TIMEOUT: 180
    labels:
      traefik.enable: true
      traefik.http.routers.traefik.rule: Host(`${APP_URL}`)
      traefik.http.services.traefik.loadbalancer.server.port: 8080

The first part describes the compose syntax version and the default network to use. I'm reusing my NPM network, as all my services are already connected to this. Using the same network across my compose files, allows me to use Traefik as a reverse proxy without opening container ports on my host.

Next we define the service. The first few rules are pretty standard. The image tag to tell Docker what image to use. The parameter restart is set to always because we want our reverse proxy to always come back up. We expose port 80 and 443 because Traefik will handle all http(s) requests to our host. In the volumes section, we give Traefik read-only access to our Docker socket so it can read the labels. We also define a storage location where it can store its SSL certificates.

Command

With docker-compose we can define extra flags and/or parameters for the start command using comand. I'll go over all these and tell what they do:

  • --api.insecure=true
    This tells Traefik to expose its dashboard using http. It will use port 8080 by default.
  • --providers.docker
    It lets Traefik know it should look for Docker images and tags to create its config.
  • --providers.docker.exposedbydefault=false
    This makes sure only containers with the label traefik.enable=true are handled by Traefik.
  • --entrypoints.web.address=:80
    Traefik creates an endpoint that will listen to requests on port 80.
  • --entrypoints.web.http.redirections.entrypoint.to=websecure
    It will send all http traffic on port 80 to another endpoint called websecure.
  • --entrypoints.websecure.address=:443
    Traefik creates an endpoint that will listen to requests on port 80.
  • --entrypoints.websecure.http.tls.domains[0].main=${DOMAIN}
    For the websecure endpoint, traefik will use a certificate for the domain saved in that variable.
  • --entrypoints.websecure.http.tls.domains[0].sans=*.${DOMAIN}
    The certificate will also be valid for the wildcard domain.
  • --entrypoints.websecure.http.tls.certresolver=myresolver
    Traefik will try to get SSL certificates from Let's Encrypt using the resolver myresolver.
  • --certificatesresolvers.myresolver.acme.dnschallenge=true
    We create a certificate resolver that will try to get a certificate using a DNS challenge. This is needed for wildcard certificates.
  • --certificatesresolvers.myresolver.acme.dnschallenge.provider=godaddy
    I use GoDaddy for my domain names and DNS, so we tell the resolver to use that.
  • --certificatesresolvers.myresolver.acme.email=${MAIL_ADR}
    Use the email address from that variable to accept the Let's Encrypt agreements.
  • --certificatesresolvers.myresolver.acme.storage=/letsencrypt
    Tell Traefik where to store its certificates.

Environment

To use the GoDaddy API for the DNS challenge, we need to define it in the environment variables of our container. The GODADDY_API_KEY and GODADDY_API_SECRET are filled from variables. These can be created in the GoDaddy developer portal. GODADDY_POLLING_INTERVAL defines the time in seconds between test to see if the DNS record has been updated. GODADDY_PROPAGATION_TIMEOUT tells Traefik how long it should wait before giving up on trying to resolve the DNS record. I had to increase these values from the defaults, as the DNS updates weren't quick enough.

Labels

Now to expose our dashboard, we need to label the container. Traefik creates its config from labels, even his own config is read from his own labels.

  • traefik.enable: true
    Tells Traefik to configure this container.
  • traefik.http.routers.traefik.rule: Host(`${APP_URL}`)
    Use the APP_URL variable to define a reverse proxy based on hostname for this container. All requests for http(s)://${APP_URL} will be send to this container.
  • traefik.http.services.traefik.loadbalancer.server.port: 8080
    The dashboard is hosted on port 8080, so using this label Traefik knows to send traffic to port 8080.

.env

As you can see, we used a few variables in our compose file. We define these in the .env file in the same folder:

DATADIR=*****
DOMAIN=*****
APP_URL=*****
MAIL_ADR=*****
GODADDY_API_KEY=*****
GODADDY_API_SECRET=*****

Fill in these variables with your config parameters and you will be able to launch Traefik using docker-compose up -d. Once it is running you should be able to reach your Traefik dashboard using the url you filled in in the APP_URL variable.

Exposing Containers

If you want to expose other containers, you can adapt its compose file and add the same three labels:

    labels:
      traefik.enable: true
      traefik.http.routers.traefik.rule: Host(`${APP_URL}`)
      traefik.http.services.traefik.loadbalancer.server.port: 8080

Just change the APP_URL for each service, and tell Traefik the right port to send the traffic to.