Skip to content

Repository files navigation

nginx-proxy-ondemand

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=true

Why?

Most 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.

How It Works

                        ┌─────────────────────────────┐
                        │   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
  1. Managed containers opt in with one label: nginx-proxy-ondemand.enable=true
  2. nginx-proxy-ondemand reads VIRTUAL_HOST automatically — no duplicate config
  3. When a container is stopped, requests fall through to the ondemand loading page
  4. The loading page polls the API and auto-redirects once the service is ready
  5. After the configured idle timeout, the container is stopped again

No shared volumes. No nginx template hacking. Logs are read via the Docker API.

Features

  • Zero config — reads VIRTUAL_HOST from 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.

Demos

Dashboard

Dashboard demo

Loading Pages

Default theme demo Minimal theme demo Hacker theme demo

Quick Start

1. Add to your nginx-proxy compose

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:ro

2. Label your services

services:
  kavita:
    image: jvmilazz0/kavita:latest
    environment:
      - VIRTUAL_HOST=kavita.example.com
      - VIRTUAL_PORT=5000
    labels:
      - nginx-proxy-ondemand.enable=true

3. Done

docker compose up -d

# Stop a managed container
docker stop kavita

# Visit kavita.example.com → loading page → auto-start → redirect

Loading Page Themes

Three 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=hacker

Or per-container:

labels:
  - nginx-proxy-ondemand.theme=hacker

Theme previews:

default

Default theme

minimal

Minimal theme

hacker

Hacker theme

Container Grouping

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=nextcloud

Visit cloud.example.com → all three start. Idle → all three stop.

Shared Dependencies

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-shared

Visiting either app starts its own group and mysql. Stopping one app leaves mysql running if another running app still depends on it.

VIRTUAL_PATH Support

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-demand

When 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.d

Pause vs Stop

Use 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=pause
stop (default) pause
Resume time Seconds (full boot) Instant (< 100ms)
RAM while idle Freed Still used
Best for Rarely used services Frequently accessed services

Dedicated API Host

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.com

The first non-.local hostname is automatically used as the API host. You can override it explicitly with ONDEMAND_API_HOST.

Configuration Reference

Environment Variables

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

Labels

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

API Endpoints

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
}

Architecture

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.

Comparison with Alternatives

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

Building

docker build -t nginx-proxy-ondemand:latest .

Or reference it directly in your compose:

ondemand:
  build: /path/to/nginx-proxy-ondemand

License

MIT

About

On-demand container startup for nginx-proxy with idle shutdown, loading pages, dashboard controls, and dependency-aware service orchestration.

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages