Stop idle containers. Start them on the first request. Zero config.
A companion container for nginx-proxy that monitors HTTP traffic and automatically stops containers when idle and starts them when someone visits their URL. A loading page is shown while the service boots, and the browser redirects once it's ready.
One label. That's all it takes.
labels:
- nginx-proxy-ondemand.enable=trueMost homelab services sit idle 99% of the time while consuming RAM and CPU. This tool lets you keep dozens of services defined and ready to go, without them all running 24/7.
- Nextcloud — only used a few times a day? Starts in seconds when you visit it.
- Jellyfin — only watch stuff in the evening? Sleeps the rest of the day.
- Dev tools, monitoring, *arr stack — spin up on demand, shut down when idle.
┌─────────────────────────────┐
│ nginx-proxy-ondemand │
│ │
Docker logs API ────>│ Log Watcher (activity) │
│ Idle Reaper (stop idle) │
Docker events ────>│ Event Watcher (discovery) │
│ Status Server (loading UI) │
└──────────┬───────────────────┘
Docker │ API
start/ │ stop
▼
Client ──> nginx (SSL) ──> nginx-proxy ──> containers
- Managed containers opt in with one label:
nginx-proxy-ondemand.enable=true - nginx-proxy-ondemand reads
VIRTUAL_HOSTautomatically — no duplicate config - When a container is stopped, requests fall through to the ondemand loading page
- The loading page polls the API and auto-redirects once the service is ready
- After the configured idle timeout, the container is stopped again
No shared volumes. No nginx template hacking. Logs are read via the Docker API.
- Zero config — reads
VIRTUAL_HOSTfrom labeled containers, no routing rules to define - Loading pages — 3 built-in themes (modern, minimal, terminal). Auto-redirect when ready.
- Container grouping — start/stop app + sidecars together as one unit
- Shared dependencies — reuse one database/cache across multiple on-demand apps via
alias+depends_on - Pause mode — sub-second resume by pausing instead of stopping (trades RAM for speed)
- VIRTUAL_PATH support — works with services sharing a hostname via
VIRTUAL_PATH(e.g./sonarr,/radarr) - Per-container overrides — different timeouts, themes, and stop methods per service via labels
- Health-check aware — waits for Docker HEALTHCHECK to pass before marking ready
- Live discovery — watches Docker events; add or remove services without restarting
- Backend readiness probe — checks the service is actually responding, not just that the container is running
- State persistence — optionally save idle timers to survive restarts
- Exclude paths — prevent health probes or uptime monitors from keeping containers awake
- Single dependency — just the Docker Python SDK. Everything else is stdlib.
services:
web:
image: nginxproxy/nginx-proxy:latest
container_name: nginx-proxy
environment:
- DEFAULT_HOST=ondemand.local # Route unknown hosts to ondemand
# ... your existing env vars
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./conf/vhost.d:/etc/nginx/vhost.d:ro
ondemand:
image: nginx-proxy-ondemand:latest
restart: unless-stopped
environment:
- VIRTUAL_HOST=ondemand.local
- VIRTUAL_PORT=8889
- ONDEMAND_PROXY_CONTAINER=nginx-proxy
- ONDEMAND_IDLE_TIMEOUT=600
- ONDEMAND_CHECK_INTERVAL=30
volumes:
- /var/run/docker.sock:/var/run/docker.sock:roservices:
kavita:
image: jvmilazz0/kavita:latest
environment:
- VIRTUAL_HOST=kavita.example.com
- VIRTUAL_PORT=5000
labels:
- nginx-proxy-ondemand.enable=truedocker compose up -d
# Stop a managed container
docker stop kavita
# Visit kavita.example.com → loading page → auto-start → redirectThree built-in themes, selectable globally or per-container:
| Theme | Description |
|---|---|
default |
Dark blue, animated spinner, status badge |
minimal |
White, clean, animated dots |
hacker |
Black terminal with green typed output, macOS-style window chrome |
Set globally:
environment:
- ONDEMAND_THEME=hackerOr per-container:
labels:
- nginx-proxy-ondemand.theme=hackerTheme previews:
default
minimal
hacker
Services that depend on each other should start and stop together. Use the group label:
services:
nextcloud:
environment:
- VIRTUAL_HOST=cloud.example.com
labels:
- nginx-proxy-ondemand.enable=true
- nginx-proxy-ondemand.group=nextcloud
redis:
labels:
- nginx-proxy-ondemand.enable=true
- nginx-proxy-ondemand.group=nextcloud
mariadb:
labels:
- nginx-proxy-ondemand.enable=true
- nginx-proxy-ondemand.group=nextcloudVisit cloud.example.com → all three start. Idle → all three stop.
Use group for containers that should behave as one unit, and alias + depends_on for shared services reused by multiple apps. depends_on entries can reference either an alias or an exact Docker container name.
services:
app01:
environment:
- VIRTUAL_HOST=app01.example.com
labels:
- nginx-proxy-ondemand.enable=true
- nginx-proxy-ondemand.group=app01
- nginx-proxy-ondemand.depends_on=mysql-shared
app02:
environment:
- VIRTUAL_HOST=app02.example.com
labels:
- nginx-proxy-ondemand.enable=true
- nginx-proxy-ondemand.group=app02
- nginx-proxy-ondemand.depends_on=mysql-shared
mysql:
labels:
- nginx-proxy-ondemand.alias=mysql-sharedVisiting either app starts its own group and mysql. Stopping one app leaves mysql running if another running app still depends on it.
Services sharing a hostname via VIRTUAL_PATH (common in *arr stacks) are fully supported:
services:
sonarr:
environment:
- VIRTUAL_HOST=arr.example.com
- VIRTUAL_PATH=/sonarr
nzbhydra:
environment:
- VIRTUAL_HOST=arr.example.com
- VIRTUAL_PATH=/nzbhydra2
labels:
- nginx-proxy-ondemand.enable=true # Only nzbhydra is on-demandWhen nzbhydra is stopped, visiting arr.example.com/nzbhydra2 shows the loading page and starts it. Other services on the same host (/sonarr, /radarr, etc.) are unaffected.
For this to work, mount the vhost.d directory into the ondemand container so it can write fallback configs:
ondemand:
environment:
- ONDEMAND_VHOST_DIR=/app/vhost.d
volumes:
- ./conf/vhost.d:/app/vhost.dUse pause for services you access frequently. Paused containers freeze in place and resume instantly, but keep using RAM.
labels:
- nginx-proxy-ondemand.enable=true
- nginx-proxy-ondemand.stop_method=pausestop (default) |
pause |
|
|---|---|---|
| Resume time | Seconds (full boot) | Instant (< 100ms) |
| RAM while idle | Freed | Still used |
| Best for | Rarely used services | Frequently accessed services |
If you expose the ondemand container on a real hostname (not just ondemand.local), the loading page will use it as a dedicated API endpoint. This is more reliable because the API stays reachable even after docker-gen switches the vhost to the real service.
environment:
- VIRTUAL_HOST=ondemand.local,ondemand.example.comThe first non-.local hostname is automatically used as the API host. You can override it explicitly with ONDEMAND_API_HOST.
Set these on the ondemand container. They define global defaults that can be overridden per-container via labels.
| Variable | Default | Description |
|---|---|---|
ONDEMAND_PROXY_CONTAINER |
nginx-proxy |
Name of the nginx-proxy container to monitor |
ONDEMAND_IDLE_TIMEOUT |
600 |
Seconds of inactivity before stopping (0 = never) |
ONDEMAND_CHECK_INTERVAL |
30 |
Seconds between idle sweeps |
ONDEMAND_STARTUP_TIMEOUT |
60 |
Max seconds to wait for container health |
ONDEMAND_STOP_METHOD |
stop |
stop or pause |
ONDEMAND_THEME |
default |
default, minimal, or hacker |
ONDEMAND_STOP_ON_STARTUP |
false |
Stop all managed containers when ondemand starts |
ONDEMAND_STATUS_PORT |
8889 |
Port for loading page and API |
ONDEMAND_STATE_FILE |
Path to persist timestamps across restarts | |
ONDEMAND_VHOST_DIR |
Path to nginx vhost.d dir (enables VIRTUAL_PATH support) | |
ONDEMAND_API_HOST |
(auto) | API hostname (auto-derived from VIRTUAL_HOST) |
ONDEMAND_LOADING_URL |
Custom external loading page URL | |
ONDEMAND_EXCLUDE_PATHS |
Comma-separated paths to ignore for activity |
Set these on managed service containers to override global defaults.
| Label | Description |
|---|---|
nginx-proxy-ondemand.enable=true |
Required for routable services. Opt in a VIRTUAL_HOST service to on-demand management |
nginx-proxy-ondemand.group=<name> |
Start/stop together with other containers in this group |
nginx-proxy-ondemand.alias=<name[,name2]> |
Stable dependency alias exposed for other services to reference |
nginx-proxy-ondemand.depends_on=<ref[,ref2]> |
Start dependency units addressed by alias or exact container name before this service; stop them when no running service still needs them |
nginx-proxy-ondemand.timeout=<seconds> |
Override idle timeout |
nginx-proxy-ondemand.startup_timeout=<seconds> |
Override startup timeout |
nginx-proxy-ondemand.stop_method=pause |
Override stop method |
nginx-proxy-ondemand.theme=hacker |
Override loading page theme |
nginx-proxy-ondemand.exclude_paths=/health,/favicon.ico |
Paths that don't count as activity |
nginx-proxy-ondemand.loading_url=<url> |
Redirect to external loading page |
The status server runs on port 8889 (configurable).
| Endpoint | Description |
|---|---|
GET /api/status/<host> |
Container state + readiness for a host |
GET /api/status/<host>?path=/foo |
Same, for VIRTUAL_PATH services |
GET /api/hosts |
List all managed hosts and their states |
GET /loading?redirect=<url> |
Loading page for VIRTUAL_PATH services |
Example response:
{
"host": "kavita.example.com",
"path": "/",
"state": "running",
"ready": true,
"idle_seconds": 42.3,
"timeout": 600
}Four threads, one Python process, one dependency (docker SDK):
| Component | Role |
|---|---|
| Log Watcher | Streams nginx-proxy access logs via Docker API. Detects requests, records activity, triggers starts. |
| Idle Reaper | Periodic sweep that stops containers exceeding their idle timeout. |
| Status Server | HTTP server serving themed loading pages and the status API. |
| Event Watcher | Watches Docker container events to live-update the host map when containers are created/destroyed. |
The DockerManager is the shared core: it maps hosts to containers, tracks idle times, resolves groups and shared dependencies, and handles the Docker API calls. Thread-safe with RLock.
| nginx-proxy-ondemand | Sablier | Lazytainer | |
|---|---|---|---|
| Reverse proxy | nginx-proxy | Traefik, Caddy, Nginx | Any |
| Detection method | Access log streaming | Middleware (holds request) | Network activity |
| Setup | 1 label per container | Provider config + labels | Labels + network config |
| Loading page | Built-in (3 themes) | Built-in (4 themes) | None |
| Container groups | Yes | Yes | Yes |
| VIRTUAL_PATH | Yes | N/A (middleware-based) | No |
| Pause support | Yes | Yes | Yes |
| Dependencies | Docker SDK (Python) | Go binary | Go binary |
docker build -t nginx-proxy-ondemand:latest .Or reference it directly in your compose:
ondemand:
build: /path/to/nginx-proxy-ondemandMIT



