Docker event watcher that automatically creates and updates Nginx Proxy Manager proxy hosts
534
Docker image: https://hub.docker.com/r/tarach/npm-docker-auto-proxy
Source code: https://github.com/tarach/npm-docker-auto-proxy
NPM Repo: https://github.com/NginxProxyManager/nginx-proxy-manager
npm-docker-auto-proxy adds Traefik-like Docker labels to Nginx Proxy Manager.
You keep using NPM as your reverse proxy and UI, but proxy hosts can be created, updated, disabled, or deleted automatically from Docker container labels.
Good for homelab / TrueNAS SCALE / Docker Compose setups where you do not want to manually click through NPM every time you add a service.
Nginx Proxy Manager is great when you want a UI for reverse proxy hosts.
But if you run many Docker services, manually creating a proxy host for every container gets repetitive.
This project keeps NPM in place and adds a small automation layer on top of it: Docker labels in, NPM proxy hosts out.
services:
jellyfin:
image: jellyfin/jellyfin
labels:
npm.proxy.enabled: "true"
npm.proxy.domain: "jellyfin.example.com"
npm.proxy.forward_host: "jellyfin"
npm.proxy.forward_port: "8096"
npm.proxy.scheme: "http"
npm.proxy.ssl: "true"
npm.proxy.certificate: "*.example.com"
npm.proxy.force_ssl: "true"
When this container starts, npm-docker-auto-proxy creates or updates the matching NPM proxy host automatically.
| Docker event | NPM action |
|---|---|
| Container starts | Creates, updates, and enables the matching proxy host |
| Container stops | Disables, deletes, or keeps the proxy host, depending on npm.proxy.on_stop |
| Container is recreated with changed labels | Updates the matching proxy host |
npm-docker-auto-proxy starts | Runs an initial scan and catches already-running containers |
LABELS_PREFIX.log/slog./var/run/docker.sock.The application listens to Docker container events:
Docker container events
↓
npm-docker-auto-proxy
↓
Docker labels
↓
Nginx Proxy Manager API
↓
Proxy Host create/update/enable/disable/delete
On startup, it scans all currently running containers.
After that, it listens for selected Docker events:
create
start
restart
die
stop
destroy
Other Docker events are ignored.
The application needs access to the Docker socket:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Access to /var/run/docker.sock is powerful. Even when mounted as read-only, the Docker API exposed by the socket can still allow privileged Docker operations depending on permissions.
For a first test, the container can run as root:
user: "0:0"
A safer setup is to run the container as a non-root user with access to the group that owns /var/run/docker.sock.
Check Docker socket ownership:
stat -c 'user=%U group=%G uid=%u gid=%g %A %n' /var/run/docker.sock
Example:
user=root group=docker uid=0 gid=999 srw-rw---- /var/run/docker.sock
This means the socket is owned by root:docker, and the Docker socket group ID is 999.
To run the container as a non-root user, add that group ID to the container:
group_add:
- "999"
NPM_BASE_URL=http://nginx-proxy-manager:81/api
[email protected]
NPM_PASSWORD=change-me
LOG_LEVEL=info
DOCKER_SOCKET_PATH=/var/run/docker.sock
LABELS_PREFIX=npm.proxy.
NPM_BASE_URLBase URL of the Nginx Proxy Manager API.
Examples:
NPM_BASE_URL=http://nginx-proxy-manager:81/api
or:
NPM_BASE_URL=http://192.168.1.2:30020/api
NPM_EMAILNginx Proxy Manager API user email.
NPM_PASSWORDNginx Proxy Manager API user password.
NPM_PASSWORD=change-me
LOG_LEVELSupported values:
debug
info
warn
error
Default:
LOG_LEVEL=info
Use debug while testing:
LOG_LEVEL=debug
DOCKER_SOCKET_PATHPath to the Docker socket inside the container.
Default:
DOCKER_SOCKET_PATH=/var/run/docker.sock
LABELS_PREFIXPrefix used for Docker label keys.
Default:
LABELS_PREFIX=npm.proxy.
With the default prefix, a container uses labels such as npm.proxy.enabled and npm.proxy.domain.
To use a custom prefix:
LABELS_PREFIX=myapp.proxy.
Then use matching labels on containers:
labels:
myapp.proxy.enabled: "true"
myapp.proxy.domain: "app.example.com"
myapp.proxy.forward_host: "app"
myapp.proxy.forward_port: "8080"
If the prefix does not end with ., a trailing dot is added automatically. For example, myapp.proxy becomes myapp.proxy..
The LABELS_PREFIX value in npm-docker-auto-proxy must match the prefix used on container labels.
Example docker-compose.yml:
services:
npm-docker-auto-proxy:
image: tarach/npm-docker-auto-proxy:latest
build: .
container_name: npm-docker-auto-proxy
restart: unless-stopped
user: "0:0"
environment:
NPM_BASE_URL: "http://192.168.1.2:30020/api"
NPM_EMAIL: "[email protected]"
NPM_PASSWORD: "change-me"
LOG_LEVEL: "info"
DOCKER_SOCKET_PATH: "/var/run/docker.sock"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy
networks:
proxy:
external: true
docker compose build --no-cache --progress=plain
docker compose up -d
docker logs npm-docker-auto-proxy
Pretty JSON logs:
docker logs npm-docker-auto-proxy | jq
Follow logs with timestamps:
docker logs -tf npm-docker-auto-proxy
Containers declare proxy settings through Docker labels. By default, label keys start with npm.proxy. (see LABELS_PREFIX).
A proxied container must have:
labels:
npm.proxy.enabled: "true"
npm.proxy.domain: "jellyfin.domain.com"
npm.proxy.forward_host: "jellyfin"
npm.proxy.forward_port: "8096"
Full example:
labels:
npm.proxy.enabled: "true"
npm.proxy.domain: "jellyfin.domain.com"
npm.proxy.forward_host: "jellyfin"
npm.proxy.forward_port: "8096"
npm.proxy.scheme: "http"
npm.proxy.websocket: "true"
npm.proxy.ssl: "true"
npm.proxy.certificate: "*.domain.com"
npm.proxy.force_ssl: "true"
npm.proxy.http2: "true"
npm.proxy.block_exploits: "true"
npm.proxy.on_stop: "disable"
TrueNAS Scale Apps expose container labels through the web UI instead of a compose file. Adding many labels by hand is slow and error-prone.
This repository includes Chrome DevTools scripts that automate filling the Labels section of a TrueNAS app edit screen. They were tested on TrueNAS 25.10.3.1 (Goldeye). Other Scale versions may work, but UI changes can break the selectors.
Scripts:
examples/TrueNAS App Labels Configurator.js
examples/truenas.js
TrueNAS App Labels Configurator.js defines labelsSetupFunc, which adds each label key/value pair and assigns it to a container.truenas.js is an example call with sample values. Copy and edit it for your app.F12 or Ctrl+Shift+I (Cmd+Option+I on macOS)F12 or Ctrl+Shift+Iexamples/TrueNAS App Labels Configurator.js and press Enter.examples/truenas.js, adjusting:
labelContainerName — the container name shown in the TrueNAS Labels UI (for example subdomain-proxy)labels — the proxy labels for that container (default prefix: npm.proxy.*; must match LABELS_PREFIX if customized)Done.Example truenas.js values:
const labelContainerName = "subdomain-proxy";
const labels = {
"npm.proxy.enabled": "true",
"npm.proxy.domain": "subdomain-proxy.domain.com",
"npm.proxy.forward_host": "192.168.1.2",
"npm.proxy.forward_port": "81",
"npm.proxy.scheme": "http",
"npm.proxy.websocket": "true",
"npm.proxy.ssl": "true",
"npm.proxy.certificate": "*.domain.com",
"npm.proxy.force_ssl": "true",
"npm.proxy.http2": "true",
"npm.proxy.block_exploits": "true",
"npm.proxy.on_stop": "disable",
};
labelsSetupFunc(labelContainerName, labels);
Notes:
labelContainerName must match the container name shown in the TrueNAS dropdown for that label row.TrueNAS App Labels Configurator.js may need adjustment.npm.proxy.enabledEnables automatic proxy management for this container.
npm.proxy.enabled: "true"
If this label is missing or not true, the container is ignored.
This safety rule also applies to stop actions. A container without:
npm.proxy.enabled: "true"
will not trigger proxy host disable/delete actions.
npm.proxy.domainDomain name for the NPM proxy host.
npm.proxy.domain: "jellyfin.domain.com"
This becomes:
"domain_names": ["jellyfin.domain.com"]
npm.proxy.forward_hostBackend hostname or IP used by NPM.
npm.proxy.forward_host: "jellyfin"
If NPM and the target container are on the same Docker network, this can be the container name or network alias.
Example with network alias:
services:
jellyfin:
networks:
proxy:
aliases:
- jellyfin
Then use:
npm.proxy.forward_host: "jellyfin"
npm.proxy.forward_portBackend port used by NPM.
npm.proxy.forward_port: "8096"
Use the internal container port, not necessarily the host-published port.
For example, Jellyfin usually listens on:
8096
inside the container.
npm.proxy.schemeBackend scheme.
npm.proxy.scheme: "http"
Default:
http
For Jellyfin, this is usually:
npm.proxy.scheme: "http"
Even if the public site uses HTTPS, the backend connection from NPM to Jellyfin is usually HTTP.
npm.proxy.websocketEnables WebSocket support.
npm.proxy.websocket: "true"
This becomes:
"allow_websocket_upgrade": true
npm.proxy.block_exploitsEnables NPM block common exploits option.
npm.proxy.block_exploits: "true"
Default:
true
npm.proxy.http2Enables HTTP/2 support on the NPM proxy host.
npm.proxy.http2: "true"
Default:
true
There are two supported ways to assign an SSL certificate to a proxy host.
Use this when the NPM API user has permission to list certificates.
labels:
npm.proxy.ssl: "true"
npm.proxy.certificate: "*.domain.com"
npm.proxy.force_ssl: "true"
The application calls:
GET /api/nginx/certificates
Then it tries to match npm.proxy.certificate against:
- certificate nice_name
- certificate domain_names
Example certificate returned by NPM:
{
"id": 3,
"provider": "letsencrypt",
"nice_name": "domain.com, *.domain.com",
"domain_names": ["*.domain.com", "domain.com"],
"expires_on": "2026-08-14 13:25:28"
}
For this label:
npm.proxy.certificate: "*.domain.com"
the application resolves:
"certificate_id": 3
and sends that to Nginx Proxy Manager.
You can also match by root domain:
npm.proxy.certificate: "domain.com"
or by nice name:
npm.proxy.certificate: "domain.com, *.domain.com"
Use this when the NPM API user cannot list certificates or when you want to avoid certificate lookup.
labels:
npm.proxy.ssl: "true"
npm.proxy.certificate_id: "3"
npm.proxy.force_ssl: "true"
npm.proxy.certificate_id is passed directly to Nginx Proxy Manager as:
"certificate_id": 3
This option does not require certificate lookup permissions.
Nginx Proxy Manager users may only see certificates they own, depending on user permissions.
If:
curl -s "http://NPM_HOST:PORT/api/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN}"
returns:
[]
then the API user probably cannot see existing certificates.
In that case, either use:
npm.proxy.certificate_id: "3"
or adjust the user permissions in NPM so the user can list the required certificate.
When SSL is enabled:
npm.proxy.ssl: "true"
one of these labels is required:
npm.proxy.certificate: "*.domain.com"
or:
npm.proxy.certificate_id: "3"
npm.proxy.force_ssl=true requires:
npm.proxy.ssl: "true"
Invalid SSL configuration prevents proxy host create/update and is logged as container_invalid_labels.
Stop behavior is controlled by:
npm.proxy.on_stop: "disable"
Supported values:
delete
del
disable
dis
off
npm.proxy.on_stop: "delete"
or:
npm.proxy.on_stop: "del"
Deletes the proxy host from Nginx Proxy Manager when the container stops.
npm.proxy.on_stop: "disable"
or:
npm.proxy.on_stop: "dis"
or:
npm.proxy.on_stop: "off"
Disables the proxy host when the container stops.
If npm.proxy.on_stop is missing, the proxy host is left unchanged when the container stops.
The application only acts on containers with:
npm.proxy.enabled: "true"
Stop actions also require:
npm.proxy.enabled: "true"
This means that a container with only:
npm.proxy.on_stop: "delete"
will not delete anything.
The application does not log:
- NPM password
- Bearer token
- Authorization header
services:
jellyfin:
image: jellyfin/jellyfin
container_name: jellyfin
restart: unless-stopped
volumes:
- /mnt/fast/apps/jellyfin/config:/config
- /mnt/tank/movies:/media
networks:
proxy:
aliases:
- jellyfin
labels:
npm.proxy.enabled: "true"
npm.proxy.domain: "jellyfin.domain.com"
npm.proxy.forward_host: "jellyfin"
npm.proxy.forward_port: "8096"
npm.proxy.scheme: "http"
npm.proxy.websocket: "true"
npm.proxy.ssl: "true"
npm.proxy.certificate: "*.domain.com"
npm.proxy.force_ssl: "true"
npm.proxy.http2: "true"
npm.proxy.block_exploits: "true"
npm.proxy.on_stop: "disable"
networks:
proxy:
external: true
Alternative SSL setup using certificate ID:
npm.proxy.ssl: "true"
npm.proxy.certificate_id: "3"
npm.proxy.force_ssl: "true"
Get a token:
TOKEN=$(
curl -s -X POST "http://192.168.1.2:30020/api/tokens" \
-H "Content-Type: application/json" \
-d '{
"identity": "[email protected]",
"secret": "change-me"
}' | jq -r '.token'
)
List proxy hosts:
curl -s "http://192.168.1.2:30020/api/nginx/proxy-hosts" \
-H "Authorization: Bearer ${TOKEN}" | jq
List certificates:
curl -s "http://192.168.1.2:30020/api/nginx/certificates" \
-H "Authorization: Bearer ${TOKEN}" | jq
To debug 502 Bad Gateway, test from inside the NPM container:
docker exec -it ix-nginx-proxy-manager-npm-1 sh
Then:
getent hosts jellyfin
curl -i http://jellyfin:8096
For Jellyfin, a good response can be:
HTTP/1.1 302 Found
Server: Kestrel
Location: web/
If this works, NPM can reach the backend.
Do not use HTTPS to the Jellyfin backend unless Jellyfin itself is configured for HTTPS:
curl -k -i https://jellyfin:8096
An error like:
wrong version number
means the backend is HTTP, not HTTPS. Use:
npm.proxy.scheme: "http"
There are other projects in this space, including:
I did not create this project as competition to them. I simply could not find them earlier when I started working on my own helper.
This implementation focuses on a small Docker-event-based companion container, explicit npm.proxy.* labels, predictable start/stop behavior, and a setup that works well for my NPM / Docker Compose / TrueNAS SCALE workflow.
Run locally:
export NPM_BASE_URL="http://192.168.1.2:30020/api"
export NPM_EMAIL="[email protected]"
export NPM_PASSWORD="change-me"
export LOG_LEVEL="debug"
export DOCKER_SOCKET_PATH="/var/run/docker.sock"
export LABELS_PREFIX="npm.proxy."
go run ./cmd/npm-docker-auto-proxy
Build binary:
CGO_ENABLED=0 GOOS=linux go build -trimpath -o npm-docker-auto-proxy ./cmd/npm-docker-auto-proxy
This project intentionally avoids switch, else, and else if in Go code.
Preferred patterns:
- early return
- map aliases
- map handlers
- small validation functions
Content type
Image
Digest
sha256:b56b8d333…
Size
8 MB
Last updated
4 months ago
docker pull tarach/npm-docker-auto-proxy