# Nginx Proxy Manager migration RustProxy ships `scripts/migration/migrate_npm.py` for a one-time, dry-run-first migration of Nginx Proxy Manager (NPM) proxy hosts from its SQLite database. The tool reads NPM only. It does not modify the NPM database and it never imports Let's Encrypt/private-key material. It logs in to the RustProxy administration API, reads the current revision, merges generated vhosts into the existing configuration, and writes only when `--apply` is supplied. `migrate_npm.py` is the CLI entry point and requires the sibling modules `scripts/migration/migrate_npm_core.py` and `scripts/migration/migrate_npm_api.py`. They use only the Python standard library, so no `pip` packages are required. If the migrator is copied or mounted outside the RustProxy source tree, keep all three files in the same directory. ## What is migrated For each NPM `proxy_host` row: - `domain_names` -> RustProxy vhost hostnames, including leading `*.` wildcards; - `forward_scheme`, `forward_host`, `forward_port` -> one RustProxy backend; - `allow_websocket_upgrade` -> vhost WebSocket policy; - `enabled` -> vhost enabled state; - `http2_support` -> vhost HTTP/2 policy; - NPM hosts with a certificate/forced SSL -> RustProxy TLS listener. Certificate files are not copied: the application-managed RustProxy default certificate is used unless `--certificate-id` names an existing RustProxy inventory certificate; - `ssl_forced` -> RustProxy `force_ssl=true`. When HTTP and HTTPS listeners exist, both are attached so cleartext requests can receive RustProxy's `308` redirect to HTTPS. If no TLS listener exists, an SSL-forced NPM host is skipped instead of being imported with weaker semantics. Migrated backends use `allow_private=true` because NPM commonly proxies to LAN or container origins. The migration tool follows RustProxy 0.22.1 and imports backend `tls_verify=false` unless you later enable verification explicitly. Use `--npm-compatible-backend-tls` when an NPM HTTPS origin depends on the common Nginx behavior of not verifying the upstream certificate; review that exception after migration. The following are reported for manual review rather than guessed: NPM access lists, advanced Nginx snippets, custom locations, NPM cache settings and `block_exploits`. Existing RustProxy hostname ownership is never silently replaced; conflicting NPM rows are skipped and shown in the report. ## SQLite location The standard NPM SQLite path inside the container is `/data/database.sqlite`. On a Docker host with a bind mount, pass the corresponding host path. For a named volume, `docker volume inspect` shows its mountpoint. Read the live mounted database rather than copying only `database.sqlite` from a running WAL-mode instance; copying without its WAL file can omit recent changes. ## Dry run Set the password in the environment or omit it and use the interactive prompt: ```bash export RUSTPROXY_USERNAME=admin export RUSTPROXY_PASSWORD='...' python3 scripts/migration/migrate_npm.py \ --database /path/to/npm/data/database.sqlite \ --rustproxy https://rustproxy.example.net:9090 ``` The default run is read-only. It creates `npm-migration-plan.json` with mode `0600` and prints every imported/skipped NPM row plus manual-review warnings. If the administration panel intentionally uses the application self-signed certificate and the script connects directly to it, add: ```bash --insecure-admin-tls ``` Prefer `--ca-file /path/to/ca.pem` or a hostname with a trusted certificate where available. By default the first enabled HTTP listener and first enabled TLS listener are used. For `ssl_forced` hosts both are selected when available. `--force-https` applies the same Force SSL policy to every imported host. Select explicit listener IDs when needed: ```bash --http-listener public-http --https-listener public-https ``` ## Apply After reviewing `npm-migration-plan.json`: ```bash python3 scripts/migration/migrate_npm.py \ --database /path/to/npm/data/database.sqlite \ --rustproxy https://rustproxy.example.net:9090 \ --apply ``` The write uses the revision returned by `GET /api/config`; a concurrent RustProxy configuration change causes the API compare-and-swap to fail rather than overwrite a newer revision. ## Running from a helper container If the Docker host does not have Python 3, mount the NPM data read-only and mount the RustProxy source/script into a temporary Python container. For example, from the RustProxy directory: ```bash docker run --rm -it \ --network host \ -v /path/to/npm/data:/npm:ro \ -v "$PWD/scripts/migration:/rustproxy-migration:ro" \ python:3.13-alpine \ python /rustproxy-migration/migrate_npm.py \ --database /npm/database.sqlite \ --rustproxy https://127.0.0.1:9090 \ --insecure-admin-tls ``` Use `--apply` only after the dry-run plan is reviewed.