Custom Tools for Kubernetes - Snooze

KubernetesCustom Tools
August 21, 2025
James Spurin
James Spurin
CNCF Ambassador • Kubestronaut • Docker Captain
Share this article:
Custom Tools for Kubernetes - Snooze

Image: It shouldn't be difficult, to run a webserver with a custom message!

Ever spun up nginx just to test a Service/Ingress/Gateway API... and 10 minutes later you're knee‑deep in default.conf, ConfigMaps, and volume mounts just to print one line?

There’s a faster way for labs and learning: snooze - a tiny HTTP server that returns whatever message you ask it to, no files required.

The "nginx quick check" that isn't

# It looks easy... until you need a custom response

docker run --rm -p 8080:80 nginx:alpine
curl http://localhost:8080

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

# Next step: craft index.html or a default.conf, mount it, restart...

The 10‑second alternative

docker run -d --rm -p 8080:80 \
  -e MESSAGE="Hello from snooze" \
  spurin/snooze:latest

curl http://localhost:8080
Hello from snooze

Want the deeper breakdown? Skip to the nginx comparison.

TL;DR
snooze is a minimalist, statically‑linked HTTP server container. By default it listens on port 80 and returns a fixed message. You can change the port and the message via env vars (PORT, MESSAGE) or flags (--port, --message). It’s perfect for learning, labs, debugging Ingress/Service/Gateway API wiring, and building mental models without app noise.


Why I built snooze

When you teach Kubernetes, a lot of the time the issue may not be "is my configuration correct?” it is "is my component doing what I think it should be doing?” For that, you want a predictable endpoint:

  • Always up, zero dependencies, minimal attack surface.
  • Easy to use, who wants to waste time on config files and mounts for a simple http response
  • Customisable Message and Port, via Environment variables and Command-line options
  • Deterministic responses (great for path‑based routing, mTLS/TLS termination checks, NetworkPolicy drills, blue/green canaries, etc.).
  • Disposable and tiny, so you can spin up lots of them to prove a point.

snooze borrows the ultra‑minimal philosophy from my idle image (which deliberately does nothing) and adds one capability: serve an HTTP response. That’s it. No framework, no baggage; just a tiny C binary in a scratch image.


What you get

  • Static binary on scratch → tiny footprint, fast pulls, minimal surface.
  • Defaults: port 80, message "Hello from snooze!".
  • Override order (highest → lowest):
    1. Environment variables: PORT, MESSAGE
    2. Command‑line flags: --port, --message
    3. Built‑in defaults
  • Graceful shutdown: handles SIGINT/SIGTERM.
  • Prebuilt image: spurin/snooze:latest.

Quick start (Docker)

Run with defaults (port 80, default message):

docker run --rm -p 80:80 spurin/snooze:latest

# In another terminal
curl http://localhost
Hello from snooze!

Override port and message with environment variables:

docker run --rm -p 8080:8080 \
  -e PORT=8080 \
  -e MESSAGE="Custom Snooze Message" \
  spurin/snooze:latest

# In another terminal
curl http://localhost:8080
Custom Snooze Message

Override with command line flags (used when the corresponding env var is not set):

# override both the port and the message
docker run --rm -p 9090:9090 spurin/snooze:latest \
  --port=9090 --message="Command line override!"

# In another terminal
curl http://localhost:9090
Command line override!

# override just the port (message stays default)
docker run --rm -p 7070:7070 spurin/snooze:latest --port=7070

# In another terminal
curl http://localhost:7070
Hello from snooze!

Why not just use nginx?

Yes, you can use nginx as a tiny web service but changing the body text at runtime is more difficult than it should be. You typically need to inject files (an index.html or a custom default.conf) via bind‑mounts/volumes or a ConfigMap. With snooze, you set an env var or a flag and you’re done.

Side‑by‑side (Docker)

snooze – custom message, custom port (no files):

docker run --rm -p 8080:8080 \
  -e PORT=8080 \
  -e MESSAGE="Hello from snooze" \
  spurin/snooze:latest

# In another terminal
curl http://localhost:8080
Hello from snooze

nginx – option A (bind‑mount a file):

# Create a one‑off index.html in the current dir
echo "Hello from nginx (mounted file)" > index.html
# Run nginx serving that file
docker run --rm -p 8080:80 \
  -v $(pwd)/index.html:/usr/share/nginx/html/index.html:ro \
  nginx:alpine

# In another terminal
curl http://localhost:8080
Hello from nginx (mounted file)

nginx – option B (custom config to return text):

cat > default.conf <<'EOF'
server {
  listen 80;
  location / {
    return 200 'Hello from nginx (config)!';
    add_header Content-Type text/plain;
  }
}
EOF

docker run --rm -p 8080:80 \
  -v $(pwd)/default.conf:/etc/nginx/conf.d/default.conf:ro \
  nginx:alpine

# In another terminal
curl http://localhost:8080
Hello from nginx (config)!

Both nginx approaches work-but they require preparing files, managing mounts, and remembering paths. For quick labs and demos, that friction adds up.

Side‑by‑side (Kubernetes)

snooze – no volumes, configure with env:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: snooze-simple
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snooze-simple
  template:
    metadata:
      labels:
        app: snooze-simple
    spec:
      containers:
      - name: snooze
        image: spurin/snooze:latest
        env:
        - name: MESSAGE
          value: "Hello from snooze in K8s"
        - name: PORT
          value: "8080"
        ports:
        - containerPort: 8080

nginx – use a ConfigMap + volume to inject content:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-index
data:
  index.html: |
    Hello from nginx (ConfigMap)!
  default.conf: |
    server {
      listen       8080;
      server_name  localhost;

      location / {
        root   /usr/share/nginx/html;
        index  index.html;
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-with-index
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-with-index
  template:
    metadata:
      labels:
        app: nginx-with-index
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: web
          mountPath: /usr/share/nginx/html
        - name: config
          mountPath: /etc/nginx/conf.d
      volumes:
      - name: web
        configMap:
          name: nginx-index
          items:
          - key: index.html
            path: index.html
      - name: config
        configMap:
          name: nginx-index
          items:
          - key: default.conf
            path: default.conf

Alternative nginx approach: mount a default.conf that uses return 200 '...'; and sets Content-Type. Still a ConfigMap + volume mount.

At a glance

Tasksnoozenginx
Change response body-e MESSAGE=... or --message=...Provide file(s) or custom nginx config
Change port-e PORT=... or --port=...Edit listen port in config or rely on container default + Service/host port
Docker quickstartSingle docker runUsually needs a file + -v mount
Kubernetes configNo volumes requiredConfigMap + volume + mount path
Moving partsminimalmultiple files/paths

This is exactly the gap snooze fills: the fastest path from idea → reachable HTTP endpoint without the scaffolding.


Kubernetes: the basics

Minimal Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: snooze
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snooze
  template:
    metadata:
      labels:
        app: snooze
    spec:
      containers:
      - name: snooze
        image: spurin/snooze:latest
        ports:
        - containerPort: 80

Expose with a Service and test:

---
apiVersion: v1
kind: Service
metadata:
  name: snooze
spec:
  selector:
    app: snooze
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  type: ClusterIP
kubectl apply -f snooze.yaml
kubectl port-forward svc/snooze 8080:80

# In another terminal
curl http://localhost:8080
Hello from snooze!

Override via flags in a Pod/Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: snooze-override-cmd
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snooze-override-cmd
  template:
    metadata:
      labels:
        app: snooze-override-cmd
    spec:
      containers:
      - name: snooze
        image: spurin/snooze:latest
        args:
          - "--port=8080"
          - "--message=Hello from command-line in K8s!"
        ports:
        - containerPort: 8080

Override via ConfigMap (great for HTML)

apiVersion: v1
kind: ConfigMap
metadata:
  name: snooze-config
data:
  message: |
    <html>
    <head><title>Snooze</title></head>
    <body>
      <h1>Hello from snooze ConfigMap!</h1>
      <p>We can store any HTML here.</p>
    </body>
    </html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: snooze-config-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snooze-config
  template:
    metadata:
      labels:
        app: snooze-config
    spec:
      containers:
      - name: snooze
        image: spurin/snooze:latest
        env:
        - name: MESSAGE
          valueFrom:
            configMapKeyRef:
              name: snooze-config
              key: message
        - name: PORT
          value: "8080"
        ports:
        - containerPort: 8080

Ingress lab: path‑based routing with three colors

Spin three snooze Deployments on different ports and route /red, /green, /blue:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: snooze-red
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snooze-red
  template:
    metadata:
      labels:
        app: snooze-red
    spec:
      containers:
      - name: snooze
        image: spurin/snooze:latest
        args: ["--port=8081", "--message=RED!"]
        ports:
        - containerPort: 8081
---
apiVersion: v1
kind: Service
metadata:
  name: snooze-red-service
spec:
  selector:
    app: snooze-red
  ports:
  - port: 80
    targetPort: 8081
    protocol: TCP
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: snooze-green
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snooze-green
  template:
    metadata:
      labels:
        app: snooze-green
    spec:
      containers:
      - name: snooze
        image: spurin/snooze:latest
        args: ["--port=8082", "--message=GREEN!"]
        ports:
        - containerPort: 8082
---
apiVersion: v1
kind: Service
metadata:
  name: snooze-green-service
spec:
  selector:
    app: snooze-green
  ports:
  - port: 80
    targetPort: 8082
    protocol: TCP
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: snooze-blue
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snooze-blue
  template:
    metadata:
      labels:
        app: snooze-blue
    spec:
      containers:
      - name: snooze
        image: spurin/snooze:latest
        args: ["--port=8083", "--message=BLUE!"]
        ports:
        - containerPort: 8083
---
apiVersion: v1
kind: Service
metadata:
  name: snooze-blue-service
spec:
  selector:
    app: snooze-blue
  ports:
  - port: 80
    targetPort: 8083
    protocol: TCP
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: snooze-colors-ingress
spec:
  rules:
  - host: snooze.example.com
    http:
      paths:
      - path: /red
        pathType: Prefix
        backend:
          service:
            name: snooze-red-service
            port:
              number: 80
      - path: /green
        pathType: Prefix
        backend:
          service:
            name: snooze-green-service
            port:
              number: 80
      - path: /blue
        pathType: Prefix
        backend:
          service:
            name: snooze-blue-service
            port:
              number: 80

What to try

curl http://snooze.example.com/red    RED!
curl http://snooze.example.com/green  GREEN!
curl http://snooze.example.com/blue   BLUE!

This is brilliant for validating path routing, service selectors, and your DNS.


When should I use idle or snooze?

  • idle (my ultra‑minimal do‑nothing container): perfect when you want a Pod to exist and do nothing (scheduling, disruption budgets, taints/tolerations demos, etc.).
  • snooze: you need a simple HTTP response to validate wiring. Minimalism!


Happy containering!

Share this article:
James Spurin

James Spurin

CNCF Ambassador • Kubestronaut • Docker Captain

James Spurin is a distinguished expert in cloud-native technologies, backed by over 25 years of professional industry experience spanning infrastructure engineering, DevOps, and modern container orchestration. As a CNCF Ambassador and Docker Captain, he regularly contributes to the community through speaking engagements, workshops, and technical content.