Docker Scale-to-Zero with Traefik and Sablier
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.
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.
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.
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.
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.