Migrating from nginx-proxy
DockYARP is built as an nginx-proxy equivalent — most of what an nginx-proxy operator already knows carries over directly. This page covers two migration paths and, separately, the two things operators worry about most: your existing certificates, and being able to roll back if something goes wrong. For the exhaustive feature-by-feature comparison, see the parity matrix in the repository rather than this page.
Either path: only the nginx-proxy stack itself is replaced. Every backend service keeps its existing
VIRTUAL_HOST/LETSENCRYPT_HOST-style labels or environment variables unchanged — DockYARP reads the same
keys (see Configuration) — so no other stack’s configuration needs to change.
Basic migration
A typical setup: nginxproxy/nginx-proxy + nginxproxy/acme-companion, public Let’s Encrypt, VIRTUAL_HOST
declared as an environment variable on each backend container (the convention nginx-proxy’s own docs use).
- Stand up DockYARP alongside the existing nginx-proxy stack, using the base stack
(a Docker socket proxy + the
dockyarpservice) — don’t stop nginx-proxy yet. - Copy your certificates into DockYARP’s certificate volume — see Certificates and rollback below before doing anything else.
- Your backend environment variables (or labels, if you use those instead) already work as-is:
VIRTUAL_HOST,VIRTUAL_PORT,LETSENCRYPT_HOST,LETSENCRYPT_EMAILare read unchanged (see Configuration for the full reference). - Point traffic (DNS, or your router/firewall’s port forward) at DockYARP instead of nginx-proxy, and verify.
- Keep the nginx-proxy stack stopped (not removed) for as long as you want the rollback option — see below.
Worked example
nginx-proxy + acme-companion, exactly as nginx-proxy’s own two-container
example documents it, plus one
backend container:
services:
nginx-proxy:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
labels:
- "com.github.nginx-proxy.nginx"
volumes:
- certs:/etc/nginx/certs:ro
- html:/usr/share/nginx/html
- /var/run/docker.sock:/tmp/docker.sock:ro
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
environment:
- DEFAULT_EMAIL=admin@example.com
volumes:
- certs:/etc/nginx/certs:rw
- html:/usr/share/nginx/html:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
app:
image: my/app
environment:
- VIRTUAL_HOST=app.example.com
- LETSENCRYPT_HOST=app.example.com
volumes:
certs:
html:
acme:
The DockYARP equivalent — nginx-proxy and acme-companion are replaced by the base stack;
app is copied over unchanged:
services:
dockerproxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: "1"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
dockyarp:
image: ghcr.io/gcelet/dockyarp # or dockyarp:local for a local build
ports: ["80:8080", "443:8443"]
environment:
Docker__Enabled: "true"
Docker__DockerEndpoint: "tcp://dockerproxy:2375"
Tls__AcmeDirectoryUri: "https://acme-v02.api.letsencrypt.org/directory"
Tls__AcceptTermsOfService: "true"
Tls__ContactEmail: "admin@example.com"
volumes:
- certs:/certs # see Certificates and rollback below before starting this for real
depends_on: [dockerproxy]
app:
image: my/app
environment:
- VIRTUAL_HOST=app.example.com
- LETSENCRYPT_HOST=app.example.com
volumes:
certs:
The admin API and dashboard are off by default (AdminApi:Surface: Disabled) — see
Examples to turn them on behind a
dedicated host. Not required for the migration itself.
Advanced migration
A more involved setup looks like this: the classic separate nginx + docker-gen + acme-companion trio
instead of the single nginxproxy/nginx-proxy image, a private ACME certificate authority instead of
public Let’s Encrypt, and possibly more than one Docker network (for example, one network for services
reachable directly on your LAN, another for the ones reverse-proxied through nginx-proxy).
The three-container split exists mainly so the official nginx image can be used directly — rather than
depending on nginxproxy/nginx-proxy’s own release cadence for nginx security patches, docker-gen regenerates
the config for a separately-managed, official nginx:alpine (or any tag you choose to track) container.
The same concern applies to DockYARP itself, in reverse: it’s a single bundled image (.NET on a
chiseled Ubuntu base), so
its own patch cadence for .NET and Linux base-image CVEs matters just as much as nginx’s did above. DockYARP
addresses this the same way any well-maintained base image should: Renovate pins the Dockerfile’s base image
to a digest and opens a PR the moment a new one is published; merging it rebuilds and republishes the image
automatically (.github/workflows/base-image-refresh.yml) — no manual tracking needed on either side.
Everything from the basic path applies. On top of that:
- A global
SSL_POLICY(e.g.Mozilla-Intermediate) set on yourdocker-gencontainer maps directly to DockYARP’sTls:SslPolicyapplication setting — same preset names. - Multiple Docker networks: DockYARP inspects its own container’s networks on startup to determine which
networks it can reach backends on (
Docker:ProxyNetworks, auto-detected when unset) — no manual network list to maintain as you add backend networks. - A private ACME certificate authority: see Private ACME certificate authority below.
Worked example
The nginx + docker-gen + acme-companion trio, exactly as nginx-proxy’s own three-container
example documents it (the
com.github.nginx-proxy.nginx/docker-gen labels are how the containers find each other — easy to miss if
you’re used to the single-container setup), plus a private CA and two backend stacks configured via
environment variables:
# front-door stack — this is the one being replaced
services:
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
labels:
- "com.github.nginx-proxy.nginx"
volumes:
- conf:/etc/nginx/conf.d:ro
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
docker-gen:
image: nginxproxy/docker-gen
container_name: nginx-proxy-gen
command: -notify-sighup nginx-proxy -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
labels:
- "com.github.nginx-proxy.docker-gen"
environment:
- SSL_POLICY=Mozilla-Intermediate
volumes:
- conf:/etc/nginx/conf.d:rw
- certs:/etc/nginx/certs:ro
- /path/to/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
environment:
- DEFAULT_EMAIL=admin@internal.example
- ACME_CA_URI=https://ca.internal.example/acme/acme/directory
- REQUESTS_CA_BUNDLE=/etc/certificates/private-root.crt
volumes:
- certs:/etc/nginx/certs:rw
- html:/usr/share/nginx/html:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
conf:
html:
certs:
acme:
Two separate backend stacks, each its own compose.yaml — untouched by the migration:
# api's own stack
services:
api:
image: my/api
environment:
- VIRTUAL_HOST=api.internal.example
- LETSENCRYPT_HOST=api.internal.example
# app's own stack
services:
app:
image: my/app
environment:
- VIRTUAL_HOST=app.internal.example
- LETSENCRYPT_HOST=app.internal.example
The DockYARP equivalent replaces only the front-door stack — api and app above are copied over unchanged:
services:
dockerproxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: "1"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
dockyarp:
image: ghcr.io/gcelet/dockyarp # or dockyarp:local for a local build
ports: ["80:8080", "443:8443"]
environment:
Docker__Enabled: "true"
Docker__DockerEndpoint: "tcp://dockerproxy:2375"
AdminApi__ApiKey: "change-me"
Tls__SslPolicy: "Mozilla-Intermediate"
Tls__AcmeDirectoryUri: "https://ca.internal.example/acme/acme/directory"
Tls__AcceptTermsOfService: "true"
Tls__ContactEmail: "admin@internal.example"
SSL_CERT_FILE: "/etc/ssl/certs/combined-ca-bundle.pem" # see Private ACME certificate authority below
volumes:
- certs:/certs
- ./combined-ca-bundle.pem:/etc/ssl/certs/combined-ca-bundle.pem:ro
depends_on: [dockerproxy]
volumes:
certs:
Docker:ProxyNetworks is left unset here on purpose — DockYARP auto-detects its own reachable networks, so
adding a second network for LAN-direct services needs no config change on the DockYARP side either.
Certificates and rollback
Copy your certificates — never move them. nginx-proxy (via acme-companion) stores certificates as
<host>.crt (the full chain) and <host>.key (the private key) in a flat directory. Copy that directory’s
contents into DockYARP’s certificate volume (Tls:CertificateDirectory, mounted at /certs in the
base stack) before DockYARP’s first start.
No format conversion is needed: DockYARP accepts the exact same <host>.crt/<host>.key shape nginx-proxy and
acme-companion already produce (a full certificate chain in the .crt file, an RSA or EC private key in the
.key file). At startup, DockYARP detects a matching <host>.crt/<host>.key pair and reuses it directly
— it does not re-issue a certificate for a host that already has a valid one, only renewing when a certificate
is within its configured renewal window of expiring.
Because this is a copy, your original nginx-proxy installation’s files are never touched. If anything looks wrong after switching traffic to DockYARP, point traffic back at nginx-proxy and restart its stack — nothing about the migration prevents that, at any point.
Your ACME account (optional, but recommended against public Let’s Encrypt)
DockYARP persists one ACME account per (contact email, ACME directory endpoint) pair and reuses it for every
certificate request and renewal, the same way acme-companion does — rather than starting fresh. If you skip
this step, DockYARP simply registers a new account on its first request; your certificates still work, but
you lose continuity with the account acme-companion already had reused across your existing certificates. Skipping
this is more of a concern against public Let’s Encrypt (which applies per-account rate limits) than against a
private CA like step-ca.
To carry the existing account over: acme-companion’s underlying acme.sh client stores each account’s key as
a PEM file (account.key) under its own persisted state, keyed by CA endpoint (and, on some installations, by
contact email). Locate it — docker exec into the acme-companion container and look under its ACME state
volume — and copy it to
{CertificateDirectory}/acme/{LETSENCRYPT_EMAIL}/{acme-directory-host}/{acme-directory-path}/account.key
(matching the certificate host you set LETSENCRYPT_EMAIL and Tls:AcmeDirectoryUri to) before DockYARP’s
first request for that host. Check the key’s algorithm first —
openssl pkey -in account.key -noout -text | head -1 should show a 256-bit key (EC P-256); DockYARP only
supports importing an EC account key today, not RSA (acme.sh’s own default when no EC key length was
explicitly requested at registration). If your key is RSA, skip this step — DockYARP will register a new
account on first use.
Private ACME certificate authority
If your certificates come from a private ACME certificate authority (for example a self-hosted step-ca
instance) rather than public Let’s Encrypt, two things need pointing at it:
The ACME directory itself — set
Tls:AcmeDirectoryUrito your CA’s ACME directory URL (the same value used asACME_CA_URIforacme-companion).Trusting the CA’s own TLS certificate — DockYARP’s ACME client needs to trust your private CA to even reach its ACME directory over HTTPS (a separate concern from trusting the certificates DockYARP will request from it). DockYARP runs on .NET, which on Linux honors the standard OpenSSL
SSL_CERT_FILEenvironment variable: mount a certificate bundle containing your private root CA (append it to a copy of the system’s default CA bundle) into the DockYARP container, and setSSL_CERT_FILEto point at it.This was verified directly rather than assumed: a throwaway, unrelated self-signed CA was tested against DockYARP’s exact container image on an isolated Docker network — the connection failed without
SSL_CERT_FILEset (confirming no certificate is trusted by default) and succeeded once it was set.
A private CA reached via HTTP-01 challenge (the default for both acme-companion and DockYARP) needs no
further configuration beyond the two points above. DNS-01 (DOCKYARP_ACME_CHALLENGE=dns-01, required for a
wildcard LETSENCRYPT_HOST) is also supported — see TLS & ACME.
DNS carried over from a split proxy/ACME setup
acme-companion and nginx itself are two separate containers with two separate network configurations —
acme-companion makes ACME calls from its own network, typically with ordinary Docker DNS (whatever the
Docker daemon forwards to). DockYARP is a single process doing both jobs, so it makes its ACME calls from the
exact same network/DNS configuration as the vhost it proxies. If nginx’s own container carried a custom dns:
override (for example one needed only because it sits on a macvlan network without Docker’s embedded DNS),
that override is now exercised for ACME calls too — and if it was never actually a working general-purpose
resolver (only ever reachable/needed for something narrower), ACME lookups for hosts outside that narrow case
will fail with a DNS resolution error, even though the same override worked fine for nginx. Point DockYARP’s
dns: at a DNS server that can resolve both internal Docker names and your real domain names before
assuming a Resource temporarily unavailable provisioning failure is a network outage.