npmplus-docker-sync

command module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 19 Imported by: 0

README ΒΆ

🏷️ npmplus-docker-sync

Add one label to a container. Get a working HTTPS proxy host in Nginx Proxy Manager.

No clicking. No copy-paste. No forgotten hosts.

CI CodeQL Go Report Card Image

Works with Nginx Proxy Manager and NPMplus Β· ~8 MB image Β· read-only Docker access

Quick start Β· Examples Β· Is it safe? Β· Configuration Β· Troubleshooting Β· Docs


πŸ€” What does it do?

You run apps in Docker and use Nginx Proxy Manager (NPM) to make them reachable under nice domains with HTTPS. Normally, every new app means opening the NPM UI, clicking through a form, picking a certificate β€” and remembering to delete the host when the app is gone.

npmplus-docker-sync does that for you. You describe the domain right next to the app, and NPM follows automatically.

services:
  whoami:
    image: traefik/whoami
    networks: [npm]
    labels:
      npm.proxy.domains: "whoami.example.com"   # ← that's it

A few seconds after docker compose up, NPM has a proxy host whoami.example.com β€” pointing at the right container and port, with the matching certificate attached and HTTPS enforced.

😩 Without😎 With npmplus-docker-sync
  1. Start the container
  2. Open the NPM UI
  3. Add proxy host, type domain
  4. Look up the container IP and port
  5. Pick the certificate, tick Force SSL
  6. Save β€” and repeat for every app
  7. Forget to delete it later
  1. Add one label
  2. docker compose up -d
  3. βœ… Done

Stop the app β†’ the host is disabled. Remove the app β†’ the host is cleaned up.

If you know Traefik's labels: it's that workflow, but for Nginx Proxy Manager β€” you keep NPM and its UI exactly as they are.

✨ Highlights

  • 🏷️ One label is enough β€” port, certificate and HTTPS are figured out automatically
  • 🧩 All four NPM types β€” proxy hosts, redirections, TCP/UDP streams and 404 hosts
  • πŸ” Certificates pick themselves β€” the best matching one, exact before wildcard
  • 🎯 No more 502 Bad Gateway β€” uses the container's IP on the network NPM can actually reach
  • πŸ›‘οΈ Hard to break β€” never touches hosts you made by hand, and refuses suspicious mass deletions
  • ⚑ Fast and quiet β€” reacts in seconds, and 20 containers starting at once cause one update
  • 🀝 Drop-in for Redth/npm-docker-sync β€” its labels work unchanged
  • πŸ”’ Secure by default β€” talks to Docker through a read-only socket proxy, runs as non-root in a ~8 MB scratch image
  • πŸ“Š Observable β€” health checks, a JSON status page and Prometheus metrics built in

πŸš€ Quick start

You need: Docker with Compose. Ready-made stacks for NPM and NPMplus live in deploy/ β€” from a two-container try-out to a hardened setup with CrowdSec:

NPM NPMplus
1 Β· Simple β€” try it out npm/1-simple npmplus/1-simple
2 Β· Standard β€” recommended npm/2-standard npmplus/2-standard
3 Β· Hardened β€” exposed servers npm/3-hardened npmplus/3-hardened
Already running NPM? existing-npm same file

1. Pick a stack

git clone https://github.com/VentumPhoenix/npmplus-docker-sync.git
cd npmplus-docker-sync/deploy/npmplus/2-standard

2. Fill in .env

cp .env.example .env    # domain of the admin UI, e-mail, password

3. Start it

docker compose up -d
docker compose logs -f npmplus-docker-sync

4. Label your apps β€” as in the example above, or start deploy/example-app. Done! πŸŽ‰

[!TIP] Adding it to an NPM that already has hosts? existing-npm starts with DRY_RUN=true: the tool only logs what it would change. Check the log, then set it to false.

Without Compose (docker run)
docker run -d --name npmplus-docker-sync \
  --network npm \
  -e DOCKER_HOST=tcp://docker-socket-proxy:2375 \
  -e NPM_URL=http://npm:81 \
  -e NPM_IDENTITY=admin@example.com \
  -e NPM_SECRET=changeme \
  -e NPM_NETWORK=npm \
  ghcr.io/ventumphoenix/npmplus-docker-sync:latest

For a quick test without a socket proxy, see deploy/npm/1-simple.

Build from source

Requires Go 1.26 or newer.

make build     # static binary ./npmplus-docker-sync
make test      # race-enabled unit tests
make check     # fmt + vet + lint + test

πŸ“– Examples

Copy what you need into the labels: of your container.

A web app with HTTPS

npm.proxy.domains: "app.example.com"

…on a specific port, with several domains

npm.proxy.domains: "app.example.com, www.app.example.com"
npm.proxy.port: "8080"

…behind a login (Authelia, Authentik, …) and an access list (login: NPMplus only)

npm.proxy.domains: "kuma.example.com"
npm.proxy.auth_request: "authelia"
npm.proxy.access_list: "Intern"

Redirect an old domain

npm.redirect.domains: "old-app.example.com"
npm.redirect.forward_domain: "app.example.com"

Expose a database as a TCP stream

npm.stream.incoming_port: "5432"

Show a 404 page for a parked domain

npm.404.domains: "parked.example.com"

Several things from one container β€” just number them:

npm.proxy.domains: "app.example.com"          # 0: the web UI
npm.1.proxy.domains: "metrics.example.com"    # 1: a second port
npm.1.proxy.port: "9090"
npm.2.stream.incoming_port: "5432"            # 2: the database

Leave a container alone

npm.enable: "false"

πŸ“š Every label β€” with what it does, its default and aliases β€” is listed in docs/FIELDS.md. The rules behind them are in docs/LABELS.md.

Label syntax in detail
npm.<kind>.<field>              one resource
npm.<index>.<kind>.<field>      several resources (npm.<kind>.<index>.<field> works too)
npm.<field>                     shorthand for npm.proxy.<field>
npm.enable=false                ignore this container
  • <kind> is proxy, redirect, stream or 404.
  • domains is the domain people visit; host is the upstream the app is served from (for redirections and 404 hosts, host means domains).
  • ., _ and - are interchangeable in field names: ssl.hsts.subdomains = ssl_hsts_subdomains.
  • Booleans accept true/false, 1/0, yes/no, on/off.
  • The prefix npm can be changed with LABEL_PREFIX.
  • A typo gets a warning with a suggestion (did you mean npm.proxy.ssl.forced?).
Automatic certificates

By default (certificate: auto) the tool picks the certificate in NPM that fits best: exact match before wildcard, then the one valid the longest. Expired certificates are ignored, and a certificate that already fits is never swapped for an equally good one. New certificates from the NPM UI are noticed within a minute.

npm.proxy.certificate: "auto"                # the default
npm.proxy.certificate: "12"                  # a fixed id
npm.proxy.certificate: "*.home.example.com"  # whatever covers this domain
npm.proxy.certificate: "name:My wildcard"    # by name
npm.proxy.certificate: "new"                 # request a Let's Encrypt one
npm.proxy.certificate: "none"                # plain HTTP

HTTPS is forced automatically as soon as a certificate is attached.

Automatic ports

If a container exposes exactly one port, you don't need a port label. With several ports, NPM_PORT_PREFERENCE (80,8080,3000,8000,443) decides. If none of them fits, the host is skipped and the log names the ports it found.

Defaults for all containers

Every label has a matching environment variable on the sync container. A label on a container always wins:

# on npmplus-docker-sync: HTTPS everywhere ...
NPM_PROXY_SSL_FORCE: "true"
# ... except on this container
npm.proxy.ssl.force: "false"

Order: indexed label β†’ label β†’ NPM_<KIND>_<FIELD> β†’ NPM_DEFAULT_<FIELD> β†’ built-in default.

NPMplus extras (groups, CrowdSec, HTTP/3, …)

Everything NPMplus can do is a label: auth_request, crowdsec_appsec, noindex, x_frame_options, fancyindex, upstream_compression, request_buffering, response_buffering, location_config, HTTP/3, custom locations β€” and group, which sorts hosts into the folders of the NPMplus UI:

npm.proxy.group: "Production"

Against the original Nginx Proxy Manager, NPMplus-only settings are simply left out and reported once β€” nothing fails.

Check labels before deploying
npmplus-docker-sync validate              # check the labels of running containers
docker compose config --format json > stack.json
npmplus-docker-sync validate stack.json   # check a stack before it starts
npmplus-docker-sync sync                  # one sync, then exit

validate never contacts NPM and exits non-zero on broken labels β€” handy as a CI step.

πŸ›‘οΈ Is it safe?

Short answer: it is built to never destroy something you care about.

Situation What happens
You created a host by hand in NPM Never deleted. Only hosts the tool created itself are ever removed.
Another tool (e.g. Redth's) created a host Never touched.
A container is stopped Its host is disabled, not deleted β€” a restart just turns it back on.
A container is restarted or updated Nothing changes (1 minute grace period).
A label contains a typo That container's hosts are protected, not deleted.
A run would delete many hosts at once Refused. (DELETE_GUARD, default: more than half)
Docker reports no containers at all Nothing is deleted.
The domain is already used by another host Reported as a conflict, nothing is overwritten.
Several Docker hosts share one NPM Each instance only manages its own hosts.
You update NPM / NPMplus Your hosts stay yours β€” see the note below.

How it recognizes its own hosts: each one gets a comment line at the top of its Advanced config in NPM β€” # npmplus-docker-sync: managed from Docker labels, do not edit or remove this line | …. nginx ignores it, this tool relies on it: leave it there. A host without it counts as hand-made.

[!IMPORTANT] Update this tool before NPMplus. The next NPMplus release clears the place where versions up to v1.0.0-beta.9 recorded their hosts; v1.0.0-beta.10 adds the line above to every host on its first run. Details

[!CAUTION] Before your first real run: if this tool ever created a host for something you still need β€” like the NPM admin UI itself β€” make sure that container still has its npm.* labels. Run once with DRY_RUN=true and check the log for would delete lines.

Docker access is read-only. The recommended setup goes through a docker-socket-proxy that only allows reading containers and events, so even a compromised sync container cannot start, stop or change anything. The container runs as non-root from an empty scratch image with a read-only filesystem, and the NPM password never appears in logs.

More: every deletion rule Β· threat model

βš™οΈ Configuration

Most setups only need these:

Variable Default What it does
NPM_URL β€” Required. Address of the NPM API, e.g. http://npm:81
NPM_IDENTITY β€” Required. NPM admin e-mail
NPM_SECRET / NPM_SECRET_FILE β€” Required. NPM admin password (or a file containing it)
NPM_NETWORK β€” The Docker network NPM and your apps share
DOCKER_HOST unix:///var/run/docker.sock How to reach Docker (tcp://docker-socket-proxy:2375 in the recommended setup)
DRY_RUN false Only log what would change
NPM_ON_STOP disable What a stopped container does to its hosts: disable, keep or delete
LOG_LEVEL info debug, info, warn, error
HEALTH_ADDR β€” Turn on /healthz, /readyz, /status, /metrics, e.g. :8080

Any variable can be read from a file by adding _FILE (Docker secrets). All variables, with aliases and tips: docs/CONFIGURATION.md.

NPM or NPMplus? Both work, detected automatically. The tool reads which fields the server accepts from the server itself, so a new NPM or NPMplus release rarely needs an update here. NPMplus-only settings (HTTP/3, CrowdSec, groups, forward auth, …) are simply left out against NPM and reported once.

[!TIP] Pin your NPM/NPMplus image to a version tag rather than :latest.

πŸ” How it works

flowchart LR
    D[🐳 Docker<br/>container events] --> L[Read labels]
    L --> W[Wait for the burst<br/>to settle Β· 3s]
    W --> C{Compare with NPM}
    C -->|new| A[Create host]
    C -->|changed| U[Update host]
    C -->|unchanged| S[Do nothing]
    C -->|container gone| R[Remove own host<br/>with safety checks]
  1. On startup it compares all labelled containers with everything in NPM and fixes the differences β€” so downtime never leaves things out of sync.
  2. Then it listens for containers starting and stopping, and syncs again within seconds.
  3. Every 5 minutes it does a full check anyway, and every minute it looks for new certificates.
  4. Unchanged hosts cost nothing β€” no API write happens unless something actually changed.

Why the container IP? NPM must reach your app, and container names only work if NPM resolves Docker's internal DNS β€” the usual cause of 502 Bad Gateway. So the upstream is the container's IP in the network NPM shares (NPM_NETWORK). How it is chosen Β· Architecture

πŸ“Š Monitoring

With HEALTH_ADDR=:8080:

Endpoint Shows
/healthz The process is alive
/readyz Docker and NPM are reachable and a sync has run
/status Every managed host as JSON, including errors and retries
/metrics Prometheus metrics (runs, errors, changes, blocked deletions, …)

The image has a built-in Docker HEALTHCHECK, so docker ps shows its health and other services can use depends_on: { condition: service_healthy }.

🩺 Troubleshooting

Nothing happens when I start a labelled container

Run with LOG_LEVEL=debug. No docker event lines? Then events aren't arriving β€” check DOCKER_HOST and that the socket proxy has EVENTS=1. Events arrive but nothing is created? Then a label is probably wrong β€” the log names it, often with a suggestion. The startup line container overview managed=… opted_out=… shows how each container was classified.

login to http://npm:81 failed

NPM_URL must point to the admin/API port (81 by default β€” not 80 or 443). Test your credentials:

curl -s -X POST http://npm:81/api/tokens \
  -H 'Content-Type: application/json' \
  -d '{"identity":"admin@example.com","secret":"changeme"}'
502 Bad Gateway, or container is not attached to <network>

NPM and your app must share a Docker network. Attach both to it and set NPM_NETWORK to its name β€” or give the host an explicit npm.proxy.host.

400 data must NOT have additional properties

The server refused a field. The tool re-reads what the server accepts right away, so the next sync usually succeeds. If it keeps failing, run with LOG_LEVEL=debug (names the fields) and please open an issue with that output.

The wrong certificate was attached

The log says why ("msg":"certificate selected", "match":"wildcard", …). Pin the one you want with npm.proxy.certificate: "<id>", or use "none" for plain HTTP.

refusing to delete: … in the log

A safety check stopped a run that would have removed too many hosts. The message says why (changed LABEL_PREFIX, no containers seen, or simply too many at once). Fix the cause β€” or, if the deletion is really intended, run once with DELETE_GUARD=off.

domain already used by another host

Another host in NPM (made by hand or by another tool) already serves that domain. The message names its id β€” remove or rename it, and the next sync creates yours.

A host disappeared after stopping a container

By default stopped containers only disable their host. Check whether NPM_ON_STOP=delete is set, and see docs/DELETION.md.

Streams or 404 hosts are not created

Check SYNC_KINDS. A warning "collection is not available on this npm instance" means your NPM doesn't offer that type.

permission denied on /var/run/docker.sock

Only with the simple stacks, which mount the socket directly: set DOCKER_GID=$(getent group docker | cut -d: -f3) in .env. The socket-proxy stacks avoid this entirely.

πŸ”„ Coming from Redth/npm-docker-sync?

Your labels and environment variables work unchanged. Both tools can even run against the same NPM without deleting each other's hosts. To take over the hosts Redth's tool created, set MIGRATE_FROM_REDTH=true β€” see docs/MIGRATION.md.

πŸ“š Documentation

Document What's in it
Labels syntax, defaults, certificates, groups, locations, streams
All label fields every label with aliases, defaults and env variables (generated)
Configuration every environment variable, health, metrics, commands
Deletion when something is deleted β€” and what prevents it
Upgrading per release, and coming from Redth/npm-docker-sync
Compatibility supported NPM/NPMplus versions, the 1.0 promise
Architecture how it works inside
Security threat model, reporting a vulnerability

πŸ—ΊοΈ Roadmap

1.0.0 βœ… β€” labels, variables and the ownership marker are stable (what that means).

Next β€” in 1.x releases, without breaking changes:

  • The next NPMplus release in the test matrix (its development branch is tested every night already)
  • Upstream modes: container name, or host IP with published ports
  • Waiting for a container to be healthy before publishing it (NPM_WAIT_HEALTHY)
  • Docker Swarm service labels
  • NPMplus mTLS client certificates and certificate requests for streams

See the changelog for what changed, and COMPATIBILITY.md for what stays stable.

🀝 Contributing

Bug reports, missing labels and pull requests are welcome β€” see CONTRIBUTING.md. If this tool saves you some clicks, a ⭐ helps others find it.

πŸ™ Acknowledgements

Built on Nginx Proxy Manager by jc21, NPMplus by ZoeyVid and docker-socket-proxy by Tecnativa. Inspired by Traefik's label-driven workflow and by Redth/npm-docker-sync. Not affiliated with or endorsed by any of these projects.

πŸ“„ License

MIT

Documentation ΒΆ

Overview ΒΆ

Command npmplus-docker-sync keeps Nginx Proxy Manager / NPMplus routing resources in sync with the labels of running Docker containers.

It is a single static binary with no runtime dependencies: point it at a Docker endpoint (ideally a docker-socket-proxy) and an NPM API, label your containers, and proxy hosts, redirections, streams and 404 hosts appear, change and disappear with them.

Directories ΒΆ

Path Synopsis
internal
certs
Package certs picks the TLS certificate of a host.
Package certs picks the TLS certificate of a host.
config
Package config loads and validates the runtime configuration from the process environment.
Package config loads and validates the runtime configuration from the process environment.
docker
Package docker turns Docker container metadata into desired NPM resource state and streams container lifecycle events.
Package docker turns Docker container metadata into desired NPM resource state and streams container lifecycle events.
fields
Package fields is the single source of truth for every configurable property of a managed NPM resource.
Package fields is the single source of truth for every configurable property of a managed NPM resource.
npm
Package npm implements a minimal, dependency-free API client for Nginx Proxy Manager and NPMplus.
Package npm implements a minimal, dependency-free API client for Nginx Proxy Manager and NPMplus.
syncer
Package syncer contains the event debouncer, the thread-safe state cache and the single-threaded reconcile worker.
Package syncer contains the event debouncer, the thread-safe state cache and the single-threaded reconcile worker.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL