Post

I Stopped Leaving My Self Hosted Apps Running All Night Now The First Request Wakes Them

I Stopped Leaving My Self Hosted Apps Running All Night Now The First Request Wakes Them

I Stopped Leaving My Self Hosted Apps Running All Night Now The First Request Wakes Them

INTRODUCTION

Self‑hosted services have become the backbone of modern homelab environments. Whether you run personal dashboards, media servers, or automation pipelines, the temptation to keep every container perpetually alive is strong. Yet this practice incurs unnecessary energy consumption, wear on hardware, and hidden operational costs.

The breakthrough came when I stopped leaving my self hosted apps running all night. Instead, I let them scale to zero and relied on an HTTP interceptor to wake them on demand. The first request after a period of inactivity triggers a pod start, services the request, and then the pod can return to a dormant state. This approach not only reduces power draw but also simplifies monitoring and extends the lifespan of underlying infrastructure.

In this guide you will learn:

  • The concepts behind scaling self hosted applications to zero.
  • How Kubernetes Event‑Driven Autoscaling (KEDA) combined with an HTTP interceptor achieves on‑demand wake‑up.
  • Step‑by‑step installation, configuration, and optimization of the solution.
  • Real‑world troubleshooting tips and performance considerations.

By the end of this article you will have a production‑ready pattern that you can adapt to any homelab or small‑scale production environment.

UNDERSTANDING THE TOPIC

What is KEDA and Why Does It Matter

KEDA (Kubernetes Event‑Driven Autoscaling) is an open‑source project that extends the Kubernetes Horizontal Pod Autoscaler (HPA) with event‑driven scaling capabilities. It watches external signals — such as message queue length, HTTP request count, or custom metrics — and adjusts replica counts accordingly.

When applied to self hosted services, KEDA can reduce the replica count to zero when no traffic is detected. The challenge is to keep the service responsive without maintaining a constantly running pod.

The HTTP Interceptor Pattern

The core of the wake‑up strategy is an HTTP interceptor placed in front of each application route. The interceptor receives the initial request, holds the connection open while the underlying pod boots, and then forwards the request once the service is ready. This pattern ensures that the first client sees no error and subsequent requests benefit from a fully warmed‑up pod.

Key components:

  • ScaledObject – a KEDA resource that defines the scaling trigger (e.g., HTTP request count).
  • HTTP Ack – a special header or response that signals the pod is ready.
  • Interceptor – a lightweight reverse proxy (often implemented with Nginx or Traefik) that manages the hold‑and‑forward logic.

Pros and Cons

AdvantagesTrade‑offs
Near‑zero idle compute costSlight latency on the first request
Reduced attack surfaceRequires careful health‑check configuration
Simpler resource planningSlightly more complex deployment manifests
Automatic scaling to zero for idle servicesDependency on external metrics for scaling decisions

Comparison to Alternatives

Traditional approaches often rely on cron jobs that ping services every few minutes to keep them alive. Those methods waste energy and cannot truly scale to zero. Other solutions like OpenFaaS or serverless frameworks provide similar wake‑up behavior but introduce additional abstraction layers. KEDA’s native Kubernetes integration makes it the most lightweight option for homelab setups.

KEDA’s ecosystem is rapidly evolving. Recent releases include support for multiple event sources, richer metric types, and tighter integration with service meshes. Expect tighter coupling with Prometheus and broader adoption of HTTP‑based scaling triggers in the next year.

PREREQUISITES

System Requirements

  • A machine running a recent version of Ubuntu LTS or CentOS 8 with at least 4 CPU cores and 8 GB RAM.
  • Docker Engine 20.10+ (if you opt for a container‑based deployment).
  • Kubernetes 1.22+ with Helm 3 installed.

Required Software

ComponentMinimum VersionPurpose
Kubernetes1.22Core orchestration platform
Helm3.9Package manager for KEDA charts
KEDA2.10Event‑driven autoscaling
Nginx (or Traefik)1.23HTTP interceptor implementation
Prometheus2.45Metric collection (optional)

Network and Security

  • Open port 80 and 443 inbound on the host firewall.
  • Ensure that the Kubernetes API server is accessible only from trusted networks.
  • Use TLS termination at the ingress layer to protect interceptor traffic.

User Permissions

  • A user with cluster-admin rights for initial installation.
  • Subsequent operations can be performed with a regular service account that has read/write access to the keda-system namespace.

Pre‑Installation Checklist

  1. Verify kubectl version --short returns a compatible version.
  2. Confirm Helm is installed (helm version).
  3. Pull the official KEDA Helm chart repository (helm repo add kedacore https://kedacore.github.io/charts).
  4. Ensure your cluster’s resource quotas allow for additional pods and services.

INSTALLATION & SETUP

Deploying KEDA

1
2
3
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda-system --create-namespace

The above command installs the KEDA control plane components into a dedicated namespace. Verify the deployment:

1
kubectl get pods -n keda-system

All pods should reach the Running state within a few seconds.

Setting Up the HTTP Interceptor

We use Nginx as the front‑proxy that holds connections open. The configuration is split into two parts: a static server block and a dynamic upstream that points to the scaling target.

Create a ConfigMap containing the Nginx configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion v1
kind ConfigMap
metadata:
  name: nginx-interceptor-config
  namespace: default
data:
  interceptor.conf: |
    http {
        upstream backend {
            server 127.0.0.1:8080;
        }
        server {
            listen 80;
            location / {
                proxy_pass http://backend;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_http_version 1.1;
                proxy_request_buffering off;
                proxy_buffering off;
                proxy_read_timeout 3600s;
                proxy_send_timeout 3600s;
                # Hold the connection open until the backend responds with X-Ready
                proxy_set_header Connection "";
                if ($sent_http_status = 502) {
                    # Wait for readiness signal before forwarding
                    proxy_pass http://backend;
                }
            }
        }
    }

Apply the ConfigMap:

1
kubectl apply -f nginx-interceptor-config.yaml

Next, deploy the Nginx pod using a Deployment that mounts the ConfigMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
apiVersion apps/v1
kind Deployment
metadata:
  name: nginx-interceptor
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-interceptor
  template:
    metadata:
      labels:
        app: nginx-interceptor
    spec:
      containers:
      - name: nginx
        image: nginx:1.23-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx/nginx.conf
          subPath: interceptor.conf
        - name: config-volume
          mountPath: /etc/nginx/conf.d
      volumes:
      - name: config-volume
        configMap:
          name: nginx-interceptor-config
---
apiVersion v1
kind Service
metadata:
  name: nginx-interceptor-svc
  namespace: default
spec:
  selector:
    app: nginx-interceptor
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

Apply the Deployment and Service:

1
kubectl apply -f nginx-interceptor.yaml

Creating a ScaledObject for a Sample Application

Assume you have a simple Flask app that exposes a /health endpoint. The goal is to scale it to zero when idle and wake on any HTTP request.

First, expose the Flask service via the interceptor. Create a Service that routes traffic to the Flask pod:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion v1
kind Service
metadata:
  name: flask-app
  namespace: default
spec:
  selector:
    app: flask-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 5000

Deploy the Flask application (omitted for brevity).

Now define a ScaledObject that watches HTTP request count on the /health path:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion keda.sh/v1alpha1
kind ScaledObject
metadata:
  name: flask-scaledobject
  namespace: default
spec:
  scaleTargetRef:
    name: flask-deployment
  minReplicaCount: 0
  maxReplicaCount: 5
  triggers:
  - type: http
    metadata:
      metadata:
        requests-path: /health
        requests-header: X-Ready
        requests-header-value: "true"
        activation-path: /wake
        activation-header: X-Wake-Event
        activation-header-value: "true"
        qps: "1"

Apply the ScaledObject:

1
kubectl apply -f flask-scaledobject.yaml

When a client sends a request to the /wake endpoint, KEDA interprets it as a trigger and scales the deployment up. The interceptor holds the connection open until the Flask pod responds with the X-Ready header, ensuring the first request is properly forwarded.

Verifying the End‑to‑End Flow

  1. Check pod status:

    1
    
    kubectl get pods -n default -l app=flask-app
    

    Initially, no pods should be running.

  2. Issue a wake request:

    1
    
    curl -X POST http://<INGRESS_HOST>/wake -H "X-Wake-Event: true"
    
  3. Observe pod creation:

    1
    
    kubectl get pods -n default -l app=flask-app -w
    

    A pod should appear and transition to Running.

  4. Make the first real request:

    1
    
    curl http://<INGRESS_HOST>/health
    

    The request should succeed, and subsequent requests will be served by the now‑warm pod.

CONFIGURATION & OPTIMIZATION

Fine‑Tuning KEDA Triggers

  • QPS (Queries Per Second) – Adjust the qps value to balance responsiveness and resource usage. A lower QPS reduces false positives but may delay scaling.
  • Activation Path – Use a dedicated path (/wake) to avoid accidental scaling from unrelated traffic.
  • Header Matching – The X-Ready header can be customized to match any internal readiness signal your application emits.

Security Hardening

  • Network Policies – Restrict traffic to the interceptor pod so only authorized clients can invoke the /wake endpoint.
  • TLS Termination – Terminate TLS at the ingress layer and enforce mutual TLS between the interceptor and backend services.
  • Rate Limiting – Apply rate limiting on the /wake endpoint to prevent abuse.

Example Nginx rate limiting snippet:

1
2
3
4
5
limit_req_zone $binary_remote_addr zone=wake:10m rate=5r/s;
location /wake {
    limit_req zone=wake burst=10 nodelay;
    # existing proxy configuration...
}

Performance Considerations

  • Connection Hold Time – The interceptor keeps connections open for the duration of pod startup. Set proxy_read_timeout and proxy_send_timeout to a value that comfortably exceeds your application’s cold‑start latency.
  • Cold‑Start Optimization – Store a small “warm” container image in a local registry to reduce image pull time. Use --platform linux/amd64 if targeting specific architectures.
  • Resource Requests – Define minimal CPU and memory requests for the scaled target to ensure quick scheduling once the pod is spawned.
1
2
3
4
5
6
7
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "256Mi"

Integration with Prometheus

If you already scrape metrics from KEDA, you can expose custom metrics for monitoring scaling events. Add the following annotation to the ScaledObject:

1
2
3
4
metadata:
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "9090"

Prometheus will then expose a metric like keda_scaledobject_status_current_replicas for Grafana dashboards.

USAGE & OP

This post is licensed under CC BY 4.0 by the author.