What You’ll Learn

  • Deploy Sablier on Kubernetes with Traefik integration
  • Configure automatic scale-to-zero for infrequently used services
  • Group related services to scale together as a unit
  • Set up on-demand service wake-up with waiting pages

Prerequisites

  • Kubernetes cluster with Traefik ingress controller
  • kubectl configured
  • Basic understanding of Kubernetes deployments

Overview

Sablier scales your services to zero replicas when idle and automatically wakes them up when accessed. Perfect for development environments, admin dashboards, and internal tools that aren’t used 24/7.

When someone accesses a scaled-down service, they see a waiting page while Sablier brings it back online in seconds.

Key concept: Services are organized into groups. All deployments in the same group scale up and down together. For example, if your application has a frontend, backend, and database, they can all be in one group - accessing any one will wake up all three.

Step 1: Enable Sablier Plugin in Traefik

Add the Sablier plugin to your Traefik Helm values:

# traefik-values.yaml
experimental:
  plugins:
    sablier:
      moduleName: "github.com/sablierapp/sablier"
      version: "v1.10.1"

Update Traefik:

helm upgrade traefik traefik/traefik -n traefik -f traefik-values.yaml

Step 2: Deploy Sablier Server

Create namespace and RBAC:

# sablier/rbac.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: sablier

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sablier
  namespace: sablier

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sablier
rules:
  - apiGroups:
      - apps
      - ""
    resources:
      - deployments
      - statefulsets
    verbs:
      - get # Retrieve info about specific dep
      - list # Events
      - watch # Events
  - apiGroups:
      - apps
      - ""
    resources:
      - deployments/scale
      - statefulsets/scale
    verbs:
      - patch # Scale up and down
      - update # Scale up and down
      - get # Retrieve info about specific dep
      - list # Events
      - watch # Events

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sablier
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: sablier
subjects:
  - kind: ServiceAccount
    name: sablier
    namespace: sablier

Deploy Sablier:

# sablier/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sablier
  namespace: sablier
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sablier
  template:
    metadata:
      labels:
        app: sablier
    spec:
      serviceAccountName: sablier
      containers:
        - name: sablier
          image: sablierapp/sablier:1.10.1
          args:
            - start
            - --provider.name=kubernetes
            - --logging.level=info
          ports:
            - containerPort: 10000
---
apiVersion: v1
kind: Service
metadata:
  name: sablier
  namespace: sablier
spec:
  selector:
    app: sablier
  ports:
    - port: 10000
      targetPort: 10000

Apply and verify:

kubectl apply -f sablier/rbac.yaml
kubectl apply -f sablier/deployment.yaml
kubectl -n sablier get pods

Step 3: Understanding Groups and Middlewares

Important: You need one Traefik middleware per group. Each middleware defines:

  • Which group it manages
  • Session duration for that group
  • Waiting page theme and settings

For example:

  • sablier-whoami middleware → manages whoami-group
  • sablier-dev-app middleware → manages dev-app-group (frontend + backend + postgres)
  • sablier-staging-app middleware → manages staging-app-group

All deployments with the same sablier.group label will scale together when any of them is accessed.

Step 4: Create Middleware for Simple Application

Let’s start with a simple single-deployment example:

# traefik/sablier-whoami-middleware.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: sablier-whoami
  namespace: traefik
spec:
  plugin:
    sablier:
      sablierUrl: http://sablier.sablier.svc.cluster.local:10000
      sessionDuration: 1m
      group: whoami-group
      dynamic:
        displayName: "Starting Whoami"
        theme: "ghost"
        showDetails: "true"
        refreshFrequency: "5s"

Apply:

kubectl apply -f traefik/sablier-whoami-middleware.yaml

Deploy the application:

# apps/whoami/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sablier-whoami
  namespace: sablier
  labels:
    app: sablier-whoami
    sablier.enable: "true"
    sablier.group: "whoami-group"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sablier-whoami
  template:
    metadata:
      labels:
        app: sablier-whoami
    spec:
      containers:
        - name: sablier-whoami
          image: acouvreur/whoami:v1.10.2
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: sablier-whoami
  namespace: sablier
spec:
  selector:
    app: sablier-whoami
  ports:
    - port: 80
      targetPort: 80

⚠️ Important: The sablier.enable and sablier.group labels must be on metadata.labels (Deployment level), not on spec.template.metadata.labels (Pod level). Sablier scans Deployment metadata, not pod labels.

Create IngressRoute using the middleware:

# apps/whoami/ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: sablier-whoami
  namespace: sablier
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`whoami.yourdomain.com`)
      kind: Rule
      priority: 10
      middlewares:
        - name: sablier-whoami
          namespace: traefik
      services:
        - name: sablier-whoami
          port: 80

Apply:

kubectl apply -f apps/whoami/

Step 5: Multi-Service Application Example

Now let’s create a complete application with frontend, backend, and database that scale together:

Create the middleware for this application:

# traefik/sablier-myapp-middleware.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: sablier-myapp
  namespace: traefik
spec:
  plugin:
    sablier:
      sablierUrl: http://sablier.sablier.svc.cluster.local:10000
      sessionDuration: 5m
      group: myapp-dev
      dynamic:
        displayName: "Starting Development App"
        theme: "hacker-terminal"
        showDetails: "true"
        refreshFrequency: "3s"

Deploy all three services with the same group:

# apps/myapp/deployments.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-frontend
  namespace: myapp
  labels:
    app: myapp-frontend
    sablier.enable: "true"
    sablier.group: "myapp-dev"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp-frontend
  template:
    metadata:
      labels:
        app: myapp-frontend
    spec:
      containers:
        - name: frontend
          image: your-frontend:latest
          ports:
            - containerPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-backend
  namespace: myapp
  labels:
    app: myapp-backend
    sablier.enable: "true"
    sablier.group: "myapp-dev"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp-backend
  template:
    metadata:
      labels:
        app: myapp-backend
    spec:
      containers:
        - name: backend
          image: your-backend:latest
          ports:
            - containerPort: 8080
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp-postgres
  namespace: myapp
  labels:
    app: myapp-postgres
    sablier.enable: "true"
    sablier.group: "myapp-dev"
spec:
  serviceName: myapp-postgres
  replicas: 1
  selector:
    matchLabels:
      app: myapp-postgres
  template:
    metadata:
      labels:
        app: myapp-postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_PASSWORD
              value: changeme
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

Create services:

# apps/myapp/services.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-frontend
  namespace: myapp
spec:
  selector:
    app: myapp-frontend
  ports:
    - port: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-backend
  namespace: myapp
spec:
  selector:
    app: myapp-backend
  ports:
    - port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-postgres
  namespace: myapp
spec:
  selector:
    app: myapp-postgres
  ports:
    - port: 5432

Create IngressRoute (only for the frontend):

# apps/myapp/ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: myapp
  namespace: myapp
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`myapp.yourdomain.com`)
      kind: Rule
      priority: 10
      middlewares:
        - name: sablier-myapp
          namespace: traefik
      services:
        - name: myapp-frontend
          port: 3000
  tls:
    secretName: myapp-tls

Now when someone accesses myapp.yourdomain.com:

  1. Sablier detects all three deployments in the myapp-dev group
  2. Scales all three (frontend, backend, postgres) to 1 replica
  3. Shows waiting page until all are ready
  4. Forwards traffic to frontend

Step 6: Test It

Check Sablier logs to see group discovery:

kubectl -n sablier logs -l app=sablier -f

Access the application:

curl https://myapp.yourdomain.com

Watch all deployments in the group scale together:

kubectl -n myapp get deployments,statefulsets -w

After 5 minutes of inactivity, all three will scale back to 0.

Configuration Options

Session Duration

Adjust per-group based on usage patterns:

spec:
  plugin:
    sablier:
      sessionDuration: 30s # Quick demos
      # sessionDuration: 5m   # Development work
      # sessionDuration: 30m  # Extended sessions

Themes

Available themes: ghost, hacker-terminal, matrix, shuffle

dynamic:
  displayName: "Starting Your App"
  theme: "matrix"
  showDetails: "true"
  refreshFrequency: "3s"

Blocking Strategy

For APIs, use blocking mode instead of showing a waiting page:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: sablier-api
  namespace: traefik
spec:
  plugin:
    sablier:
      sablierUrl: http://sablier.sablier.svc.cluster.local:10000
      sessionDuration: 10m
      group: api-group
      blocking:
        timeout: 60s

The HTTP request will wait (up to 60s) until all services in the group are ready.

Multiple Environments Example

Create separate groups and middlewares for different environments:

# Dev environment
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: sablier-myapp-dev
  namespace: traefik
spec:
  plugin:
    sablier:
      group: myapp-dev
      sessionDuration: 5m
      # ... rest of config
---
# Staging environment
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: sablier-myapp-staging
  namespace: traefik
spec:
  plugin:
    sablier:
      group: myapp-staging
      sessionDuration: 10m
      # ... rest of config

Tag deployments accordingly:

# Dev deployments
metadata:
  labels:
    sablier.enable: "true"
    sablier.group: "myapp-dev"
---
# Staging deployments
metadata:
  labels:
    sablier.enable: "true"
    sablier.group: "myapp-staging"

Resources


Series Navigation:

Questions? Drop a comment below!