Deploying API Services with Docker Swarm and Traefik

May 30, 2020

In my previous post Add HTTPS support for your app backend API in 5 mins with traefik as a reverse proxy, I demonstrated how to deploy a service with docker-compose and Traefik. But docker-compose is really meant to be use in development, not production, although it still works decently for small apps.

So in this post, I will show you how to make some modifications to the docker-compose file so you can deploy your services with docker swarm mode, which supports multiple instances of docker containers running on different machines and scales better for larger apps.

I recommend read my previous post and get you app running with docker-compose before reading this post.

Prerequisites

  • everything used in the previous post
  • make sure you have a folder name letsencrypt
  • if you haven't enabled swarm mode, run docker swarm init

Update docker-compose

Add a network at the bottom of the file. This is necessary. For some reason, the default network doesn't seem to work in swarm mode.

+ networks:
+   overlay:

Under command of the service traefik, add

  traefik:
    image: "traefik:v2.2"
    container_name: "traefik"
    command:
      #- "--log.level=DEBUG"
      - "--api.insecure=true"
      - "--providers.docker=true"
+     - "--providers.docker.swarmMode=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
    #   - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.myresolver.acme.email=johndoe@gmail.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"

Under the networks, add

+ networks:
+   - overlay:

This will put traefik on the overlay network, allowing traefik to communicate with whatever services you're running, which we will put on overlay as well.

Finally, add deploy under traefik. Note that traefik needs to be put on a manager node on the swarm, thus the constraint node.role == manager. And we only want 1 traefik instance on the swarm, so we're using global mode. Learn more here

+    deploy:
+      mode: global
+      placement:
+        constraints:
+          - node.role == manager
+      update_config:
+        parallelism: 1
+        delay: 10s
+      restart_policy:
+        condition: on-failure

That's it for the traefik service! Now let's handle your api service. Remember to replace example.com with your domain name. In this part we

  1. put your api service on overlay
  2. add the deploy section to specify configs for deployment
  api:
+    networks:
+      - overlay
+    deploy:
+      replicas: 1
+      update_config:
+        parallelism: 2
+        delay: 10s
+      restart_policy:
+        condition: on-failure
+      labels:
+        - "traefik.enable=true"
+        - "traefik.http.routers.api.rule=Host(`example.com`)"
+        - "traefik.http.routers.api.entrypoints=websecure"
+        - "traefik.http.routers.api.tls.certresolver=myresolver"
+        - "traefik.http.services.api.loadbalancer.server.port=80"
+        - "traefik.docker.network=api_overlay"

You've probably noticed the labels are very similar to the old laybels we add to the container in the previous post. The reason is that when using swarm mode, traefik loads from service labels instead of container labels, so the labels we set for the container will NOT be read by traefik.

Also notice that we added - "traefik.http.services.api.loadbalancer.server.port=80" This tells traefik which port it should use to communicate with api.(the port your docker container exposes). We didn't need to specify the port when using docker-compose because traefik automatically detects the port. However, in swarm, you have to explicitly tell traefik which port to use.

- "traefik.docker.network=biased-api_overlay" on the other hand, tells traefik which network to use to communicate with the service. Make sure to prefix the network name, in this case, overlay, with the name of your stack.(You can specify your stack name when you run the docker stack deploy command later)

Final Result

version: "3.3"

services:

  traefik:
    image: "traefik:v2.2"
    container_name: "traefik"
    command:
      #- "--log.level=DEBUG"
      - "--api.insecure=true"
      - "--providers.docker=true"
+     - "--providers.docker.swarmMode=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
    #   - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.myresolver.acme.email=johndoe@gmail.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
+   networks:
+     - overlay
+   deploy:
+     mode: global
+     placement:
+       constraints:
+         - node.role == manager
+     update_config:
+       parallelism: 1
+       delay: 10s
+     restart_policy:
+       condition: on-failure

  api:
    image: "jamesku/api"
    container_name: "api"
    ports:
        - "5001:80"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`example.com`)"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.tls.certresolver=myresolver"
+   networks:
+     - overlay
+   deploy:
+     replicas: 1
+     update_config:
+       parallelism: 2
+       delay: 10s
+     restart_policy:
+       condition: on-failure
+     labels:
+       - "traefik.enable=true"
+       - "traefik.http.routers.api.rule=Host(`example.com`)"
+       - "traefik.http.routers.api.entrypoints=websecure"
+       - "traefik.http.routers.api.tls.certresolver=myresolver"
+       - "traefik.http.services.api.loadbalancer.server.port=80"
+       - "traefik.docker.network=api_overlay"
+networks:
+  overlay:

Deployment

Now that everything is ready, we can now run the deploy command.

  • deploy docker stack deploy -c docker-compose.yml <stack name>
  • check the status of the stack docker stack ps <stack name>
  • list services docker service ls

If you want to stop your service, run docker stack rm <stack name>

That's it. You've successfully deployed your app with docker swarm. You can easily scale it by adding more nodes to your swarm and changing the replicas parameter.

Subscribe to my email list

© 2023 ALL RIGHTS RESERVED