When operating web applications or services that are only used sporadically, it can be useful to start them only when there is a specific need. In the context of Docker containers and Kubernetes, the concept of “Scale-to-Zero” exists for such use cases, which means scaling down workloads to zero.

For HTTP services in conjunction with a reverse proxy, Sablier offers a simple way to implement Scale-to-Zero in your own infrastructure.

Scale-to-Zero

Scale-to-Zero refers to the complete scaling down of workloads to zero, so that no resources are being consumed. In the context of Kubernetes, solutions like Keda exist, but they are more geared towards event-driven use cases and are not primarily suitable for HTTP services. The HTTP Addon for Keda is currently in the beta stage.

Fully shutting down applications can be advantageous when operating a large number of services with only a few requests each. This reduces the number of concurrently running containers and resource consumption. If you’re using a cloud provider and paying per resource or time unit, this can lead to cost savings.

The example code for this post can be found on Github: https://github.com/davull/demo-docker-traefik-sablier

Sablier and Traefik

Sablier is a lightweight solution for starting and stopping HTTP services on demand. It supports various container providers, currently Docker, Docker Swarm, and Kubernetes. To scale up and down the corresponding containers based on incoming requests, there are plugins available for the reverse proxies Traefik, Nginx, and Caddy. Since Sablier is defined as an API, it can theoretically be integrated into other systems as well.

Reverse Proxy Integration

Source https://acouvreur.github.io/sablier/

Below, the use of Sablier with Traefik and Docker is described. However, the configuration for other reverse proxies and container providers is analogous.

The starting point is a Traefik instance and a workload that should be controlled via Sablier. The definition of the services is done using Docker Compose.

The configuration file docker-compose-traefik.yaml for Traefik contains a single service description and a network:

version: "3"

services:
  traefik:
    container_name: traefik
    image: traefik:v2.10
    ports:
      - 80:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik/traefik.yml:/etc/traefik/traefik.yml
      - ./traefik/dynamic_config/:/etc/traefik/dynamic_config/
      - ./traefik/log/:/etc/traefik/log/
    networks:
      - traefik
    ...
    
networks:
  traefik:
    name: traefik

The workload consists of three services (all represented by traefik/whoami images), which are meant to simulate a distributed application. The file docker-compose-whoami.yaml has the following content:

version: "3"

services:
  whoami:
    container_name: whoami
    image: traefik/whoami
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami-router.rule=Host(`whoami.example.com`)"
      - "traefik.http.routers.whoami-router.entrypoints=web"

  whoami-nginx:
    container_name: nginx
    image: traefik/whoami
    networks:
      - traefik

  whoami-mariadb:
    container_name: mariadb
    image: traefik/whoami
    networks:
      - traefik

networks:
  traefik:
    name: traefik
    external: true

Setting up Sablier

Sablier itself can be run as a Docker container or as a binary. Here, we’re using the container and extending the file docker-compose-traefik.yaml with an additional service description:

sablier:
  container_name: traefik-sablier
  image: acouvreur/sablier:1.3.0
  command:
    - start
    - --provider.name=docker
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
  networks:
    - traefik

Sablier requires access to the Docker host service, which is why we’re mounting the path /var/run/docker.sock to the same location in the container. Using the parameter --provider.name, we specify the used container provider as docker.

Adding the Traefik Plugin

To enable Traefik to interact with the Sablier API and be aware of it, we configure the Traefik plugin for Sablier. For this purpose, the configuration file traefik.yml is extended with the following lines:

experimental:
  plugins:
    sablier:
      moduleName: "github.com/acouvreur/sablier"
      version: "v1.3.0"

Preparing Configuration

In order for Sablier to start and stop the containers in our workload, two adjustments need to be made.

Since Traefik no longer has access to container labels when a container is not running (scaled down to zero), we first need to change the configuration of the workload service from container labels to a configuration file. This is only necessary if container labels were used for Traefik configuration.

The labels from the workload service are transferred to a file dynamic_config/whoami.yaml.

From

# docker-compose-whoami.yaml
...
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.whoami-router.rule=Host(`whoami.example.com`)"
  - "traefik.http.routers.whoami-router.entrypoints=web"

it becomes

# dynamic_config/whoami.yaml
http:
  services:
    whoami-service:
      loadBalancer:
        servers:
          - url: http://whoami:80

  routers:
    whoami-router:
      rule: "Host(`whoami.example.com`)"
      service: whoami-service
      entryPoints:
        - web

Afterwards, we can proceed with integrating Sablier.

Configuring Sablier

Sablier has two types of strategies for responding to an incoming HTTP request when the corresponding container is not running:

  • Dynamic Strategy: Sablier serves a status webpage that informs the user their requested service is being started and automatically redirects them or refreshes the page after the start.
  • Blocking Strategy: Sablier blocks the request until the underlying container is started and then delivers the response.

The Blocking Strategy is especially suitable for APIs, so that the calling client only experiences a longer response time on the first access to a shutdown service, without needing to deal with retries and redirects.

Dynamic Strategy

To configure our workload with the Dynamic Strategy, we start by creating a Traefik middleware for Sablier. This is defined in the dynamic_config/sablier.yaml file:

http:
  middlewares:
    sablier-dynamic:
      plugin:
        sablier:
          sablierUrl: http://sablier:10000
          sessionDuration: 1m
          names: whoami,nginx,mariadb
          dynamic:
            displayName: whoami
            refreshFrequency: 1s
            showDetails: true
            theme: shuffle

sablierUrl points to our Sablier container, which we defined in the docker-compose-traefik.yaml file, and port 10,000 is the default port.

The sessionDuration parameter indicates how long a container should remain running after the last access to it. After that, the container will be shut down.

With names, we specify the names of the containers that Sablier should control. These names must match the names of the containers in our docker-compose-whoami.yaml file (parameter container_name).

Under the dynamic key, the behavior of the Dynamic Strategy is configured. We can provide a display name (displayName) for the status page, set the refresh frequency (refreshFrequency) of the status page, decide whether to show details (showDetails), and specify which theme (theme) to use. There are several themes to choose from, but custom themes can also be defined.

Dynamic Strategy Shuffle Theme

Now we configure our workload to use the Sablier middleware. In the dynamic_config/whoami.yaml file, we modify the definition of the whoami router and add the middleware:

http:
  ...
  routers:
    whoami-router:
      rule: "Host(`whoami.example.com`)"
      service: whoami-service
      entryPoints:
        - web
      middlewares:
        - sablier-dynamic@file

With this, Sablier is ready to receive our request, start the underlying container for us, and shut it down after a minute of inactivity.

Dynamic Strategy

A note about the Dynamic Strategy: Unfortunately, the Safari browser on iOS devices doesn’t display the status page on the first access and instead gives a This site can't be reached error.

Blocking Strategy

The Blocking Strategy is configured in a similar way to the Dynamic Strategy. We start by creating another Traefik middleware for Sablier. This is defined in the dynamic_config/sablier.yaml file as well:

http:
  middlewares:
    sablier-dynamic:
      ...

    sablier-blocking:
      plugin:
        sablier:
          sablierUrl: http://sablier:10000
          sessionDuration: 1m
          names: whoami
          blocking:
            defaultTimeout: 10s

The defaultTimeout specifies how long to wait for the target container to start before aborting the request.

In the dynamic_config/whoami.yaml file, we use the new middleware:

http:
  ...
  routers:
    whoami-router:
      ...
      middlewares:
        - sablier-blocking@file

When we call our workload with the Blocking Strategy, on the first access, we experience a longer response time as Sablier needs to start the underlying container.

Blocking Strategy

Conclusion

Sablier offers a straightforward way to implement Scale-to-Zero for HTTP services. Once you’ve gathered the necessary information from the various documentation sources for Sablier, Traefik, and the Traefik plugin, the configuration isn’t overly complex. However, the documentation can sometimes be thin, and you might encounter empty pages here and there.

The example code with commits for each configuration step can be found on GitHub: https://github.com/davull/demo-docker-traefik-sablier.