Sign inSign up

tarach/npm-docker-auto-proxy

By tarach

•Updated 4 months ago

Docker event watcher that automatically creates and updates Nginx Proxy Manager proxy hosts

Image
0

534

tarach/npm-docker-auto-proxy repository overview

⁠npm-docker-auto-proxy

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⁠

⁠TL;DR

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.

⁠Why?

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.

⁠Example
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.

⁠What happens on Docker events?
Docker eventNPM action
Container startsCreates, updates, and enables the matching proxy host
Container stopsDisables, deletes, or keeps the proxy host, depending on npm.proxy.on_stop
Container is recreated with changed labelsUpdates the matching proxy host
npm-docker-auto-proxy startsRuns an initial scan and catches already-running containers

⁠Table of contents

⁠Features

  • Watches Docker container events.
  • Performs an initial scan of already running containers.
  • Creates Nginx Proxy Manager proxy hosts.
  • Updates existing proxy hosts when labels change.
  • Enables proxy hosts when containers start.
  • Disables or deletes proxy hosts when containers stop.
  • Supports SSL certificates by certificate name/domain or certificate ID.
  • Supports a configurable Docker label prefix through LABELS_PREFIX.
  • Uses structured JSON logs through Go log/slog.
  • Uses Docker Engine HTTP API through /var/run/docker.sock.

⁠How it works

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.

⁠Docker socket access

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"

⁠Environment variables

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_URL

Base 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_EMAIL

Nginx Proxy Manager API user email.

⁠NPM_PASSWORD

Nginx Proxy Manager API user password.

NPM_PASSWORD=change-me
⁠LOG_LEVEL

Supported values:

debug
info
warn
error

Default:

LOG_LEVEL=info

Use debug while testing:

LOG_LEVEL=debug
⁠DOCKER_SOCKET_PATH

Path to the Docker socket inside the container.

Default:

DOCKER_SOCKET_PATH=/var/run/docker.sock
⁠LABELS_PREFIX

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

⁠Docker Compose

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

⁠Build

docker compose build --no-cache --progress=plain

⁠Run

docker compose up -d

⁠Logs

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

⁠Container labels

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

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.
⁠Configure labels in the TrueNAS UI
  1. Open the TrueNAS web UI and go to Apps.
  2. Open the app you want to configure and click Edit.
  3. Scroll to the Labels section and leave that section visible on screen.
  4. Open the browser developer tools:
    • Chrome / Edge: F12 or Ctrl+Shift+I (Cmd+Option+I on macOS)
    • Firefox: F12 or Ctrl+Shift+I
  5. Open the Console tab.
  6. Paste the contents of examples/TrueNAS App Labels Configurator.js and press Enter.
  7. Edit and paste the contents of 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)
  8. Press Enter and wait until the console prints Done.
  9. Review the filled labels in the UI, then save and redeploy the app as usual.

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:

  • Run the script only on the app Edit screen while the Labels section is present.
  • labelContainerName must match the container name shown in the TrueNAS dropdown for that label row.
  • The script adds labels; it does not remove existing ones. Clear unwanted labels manually before running it if needed.
  • After the script finishes, confirm the values in the UI and save the app so Docker receives the updated labels.
  • If TrueNAS updates its UI, the selectors in TrueNAS App Labels Configurator.js may need adjustment.

⁠Supported labels

⁠npm.proxy.enabled

Enables 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.domain

Domain name for the NPM proxy host.

npm.proxy.domain: "jellyfin.domain.com"

This becomes:

"domain_names": ["jellyfin.domain.com"]
⁠npm.proxy.forward_host

Backend 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_port

Backend 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.scheme

Backend 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.websocket

Enables WebSocket support.

npm.proxy.websocket: "true"

This becomes:

"allow_websocket_upgrade": true
⁠npm.proxy.block_exploits

Enables NPM block common exploits option.

npm.proxy.block_exploits: "true"

Default:

true
⁠npm.proxy.http2

Enables HTTP/2 support on the NPM proxy host.

npm.proxy.http2: "true"

Default:

true

⁠SSL certificates

There are two supported ways to assign an SSL certificate to a proxy host.

⁠Option 1: certificate name or domain

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"
⁠Option 2: certificate ID

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.

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

⁠SSL validation rules

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

Stop behavior is controlled by:

npm.proxy.on_stop: "disable"

Supported values:

delete
del
disable
dis
off
⁠Delete on stop
npm.proxy.on_stop: "delete"

or:

npm.proxy.on_stop: "del"

Deletes the proxy host from Nginx Proxy Manager when the container stops.

⁠Disable on stop
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.

⁠No stop action

If npm.proxy.on_stop is missing, the proxy host is left unchanged when the container stops.

⁠Safety rules

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

⁠Example: Jellyfin

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"

⁠Testing NPM API access

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

⁠Testing backend reachability from NPM

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.

⁠Development

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

⁠Notes

This project intentionally avoids switch, else, and else if in Go code.

Preferred patterns:

- early return
- map aliases
- map handlers
- small validation functions

Tag summary

Content type

Image

Digest

sha256:b56b8d333…

Size

8 MB

Last updated

4 months ago

docker pull tarach/npm-docker-auto-proxy