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 port80
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):
- Environment variables:
PORT
,MESSAGE
- Command‑line flags:
--port
,--message
- Built‑in defaults
- Environment variables:
- 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 usesreturn 200 '...';
and setsContent-Type
. Still a ConfigMap + volume mount.
At a glance
Task | snooze | nginx |
---|---|---|
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 quickstart | Single docker run | Usually needs a file + -v mount |
Kubernetes config | No volumes required | ConfigMap + volume + mount path |
Moving parts | minimal | multiple 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!
Links
- Repo: https://github.com/spurin/snooze
- Image:
spurin/snooze:latest
- Sister project: https://github.com/spurin/idle
Happy containering!