Fully automated Nginx reverse proxy for Docker and Swarm.
No more writing configuration files. nginx-proxy watches for container changes and automatically generates reverse proxy configurations. It supports SSL via Let's Encrypt, Basic Auth, and custom Nginx directives—all configured through environment variables.
- Zero-config Nginx: Automatically discovers and routes traffic to containers and services
- SSL Automation: Automatic SSL certificates via Let's Encrypt (ACME).
- Flexible Routing: Host-based, path-based, and multiple domains per container.
- Advanced Features: Sticky sessions, basic auth, redirects, and custom Nginx directives.
- Swarm Ready: Compatible with Docker Swarm services.
nginx-proxy is built on the belief that infrastructure should be invisible. Each service keeps its configuration.
Note When migrating to v3, old volumes should be removed.
docker network create frontend
docker run -d --restart always \
--name nginx-proxy \
--network frontend \
-p 80:80 \
-p 443:443 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v nginx_data:/etc/nginx \
-v nginx_logs:/var/log/nginx \
mesudip/nginx-proxy:v3Start a container on the same network and set the VIRTUAL_HOST environment variable.
docker run -d \
--name wordpress \
--network frontend \
-e VIRTUAL_HOST="blog.example.com" \
wordpressdocker run -d \
--name registry \
--network frontend \
-e VIRTUAL_HOST="htttps://registry.example.com/v2" \
-e PROXY_BASIC_AUTH="registry.example.com -> user1:pass1,user2:pass2" \
-e "client_max_body_size=2g" \
registry:2/etc/nginx/conf.d– rendered configs (nginx-proxy.conf) plus any custom files you mount./etc/nginx/ssl– certificate material, split into/certsand/private(override withSSL_DIR,SSL_CERTS_DIR,SSL_KEY_DIR)./etc/nginx/challenges– ACME challenge payloads, configurable viaCHALLENGE_DIRandWELLKNOWN_PATH./var/log/nginx– access/error logs.
Control the default behavior of nginx-proxy:
| Variable | Default | Description |
|---|---|---|
CLIENT_MAX_BODY_SIZE |
1m |
Default max body size for uploads. |
NGINX_WORKER_PROCESSES |
auto |
Number of Nginx worker processes. |
NGINX_WORKER_CONNECTIONS |
65535 |
Max connections per worker. |
CERT_RENEW_THRESHOLD_DAYS |
30 |
By default certificates are renewed when they have <=30 days remaining. |
ENABLE_IPV6 |
false |
Enable IPv6 support on nginx. |
DOCKER_SWARM |
ignore |
Controls Docker Swarm discovery. Supported values are ignore, exclude, enable, prefer-local, and strict; see Docker Swarm Support. |
SWARM_DOCKER_HOST |
- | URL of the Swarm manager socket (e.g., tcp://manager:2375). |
CERTAPI_URL |
- | External Certificate API origin, e.g. http://certapi:8080 or https://certapi.example.com. Paths, query strings, fragments, and embedded credentials are not supported. |
CERTAPI_BATCH_DOMAINS |
true |
Split certificate requests into safe batches, for getssl and for automatic issuance and renewal, with local ACME or CERTAPI_URL. With false, all due domains go into one certificate. |
NGINX_RESOLVER |
/etc/resolv.conf nameservers |
Resolver used by nginx for runtime DNS lookups when proxying ACME challenges to CERTAPI_URL. When no resolver is detected or configured, nginx renders the literal CERTAPI_URL in proxy_pass so startup fails visibly if that host cannot be resolved. Set this explicitly if automatic resolver detection is not correct for your network mode. |
CHALLENGE_DIR |
/etc/nginx/challenges/ |
Base directory for acme challenge store, when requesting certificates with acme. .well-known/acme-challenge folder lives inside this. |
CLOUDFLARE_API_KEY_KEY* |
- | Cloudflare api keys to issue DNS certificates. |
BACKEND_START_GRACE_SECONDS |
10 |
Delay registering containers without a Docker healthcheck so crashing backends dont' result reload |
STATIC_SITE_ROOT |
/static |
Directory scanned for static sites. Each domain is served from $STATIC_SITE_ROOT/$domain/current. |
DEFAULT_SSL_DOMAINS |
- | Comma-separated HTTPS domains to track and renew including wildcards like *.example.com. Additonally serves a default page when not used by containers. |
To expose a container, set the VIRTUAL_HOST* environment variable. The container must be on the same Docker network as nginx-proxy.
VIRTUAL_HOST=[<scheme>://] <domain> [<path>] [-> [:<port>] <path>] [; <nginx_directives>]
| VIRTUAL_HOST | Description |
|---|---|
example.com |
Proxies to exposed container port. |
example.com -> :8080 |
Proxies to port 8080. |
https://example.com |
HTTPS proxy to container exposed http port |
https://example.com/api |
Only /api path is passed on to container. /api prefix is not removed |
https://example.com/api -> /v1 |
Re-maps path /api to /v1 on container exposed port |
https://example.com/api -> :8080/v1 |
Re-maps path /api to /v1 on container port 8080 |
https://example.com/api -> https://_:8080/v1 |
Re-maps path /api to /v1 on port 8080. Https is used to connect to container |
Note: Directives separated by ; are injected into the Nginx location block.
Example: VIRTUAL_HOST=site.com; proxy_read_timeout 900;
nginx-proxy always reserves /.well-known/acme-challenge/ on HTTP port 80 for ACME HTTP-01 validation. Other /.well-known/... routes are supported normally through path-based VIRTUAL_HOST entries because Nginx will prefer the more specific ACME location only for challenge requests.
WebSocket support requires explicit configuration if the protocol is not detected automatically.
- Explicit:
VIRTUAL_HOST=wss://ws.example.com -> :8080/websocket - Auto-Upgrade:
VIRTUAL_HOST=https+wss://example.com(Supports both HTTP and WSS on te host).
A single container can serve multiple domains path mappings. All that matters is that env variable starts with VIRTUAL_HOST e.g. VIRTUAL_HOST1, VIRTUAL_HOST2, etc.
Example:
docker run -d --network frontend \
-e "VIRTUAL_HOST_API=https://api.example.com -> :3000" \
-e "VIRTUAL_HOST_ADMIN=https://admin.example.com -> :4000" \
my-appProxy to external hosts, (not in Docker) using STATIC_VIRTUAL_HOST. The container must be running for the site to be live.
Format: STATIC_VIRTUAL_HOST=domain.com->http://192.168.0.1:8080.
Note Be aware that if domain as target, nginx will crash if DNS resolution fails.
Serve static files directly from nginx-proxy by placing domain directories under STATIC_SITE_ROOT. STATIC_SITE_ROOT defaults to /static.
Files are served from $STATIC_SITE_ROOT/$domain/current. For example, /static/example.com/current hosts https://example.com.
STATIC_SITE_ROOT may contain only letters, numbers, /, ., _, and -; paths with spaces or other special characters are rejected and all static sites are ignored.
current can be a symlink to an active release directory inside STATIC_SITE_ROOT, which allows versioned deploys by switching the symlink and issuing a reload/HUP. Symlinks that resolve outside STATIC_SITE_ROOT are ignored. Static site locations also render disable_symlinks on from=$document_root; so symlinks inside the served document root cannot expose files outside the site. Static site directories are scanned only during a full reload. Invalid domain directory names are ignored with a warning.
An optional nginx_vhost.conf beside current ($STATIC_SITE_ROOT/$domain/nginx_vhost.conf) adds nginx directives to the site's location /, for example expires 1h;. It is included as written, so treat it like the proxy's own nginx configuration. Each static site is validated on its own: a site whose generated config fails nginx -t is skipped with a warning naming it, and the others are still served.
Static sites are mounted at /. Container-backed proxy locations can still be configured on more specific paths such as /api or /admin. If a container configures / for the same domain, that container route overrides the static site root and nginx-proxy logs a warning.
Warning : Automatic exposed port detection will not work when swarm support is enabled. You must explicitly set port on the VIRTUAL_HOST.
Docker Swarm discovery is controlled by the DOCKER_SWARM environment variable on the nginx-proxy container.
DOCKER_SWARM value |
Local containers | Swarm services | Use case |
|---|---|---|---|
ignore |
Included | Not discovered | Default Docker-only behavior. Swarm task containers are treated like standalone containers if they are visible on the local Docker socket. |
exclude |
Included | Not discovered | Docker-only discovery while explicitly ignoring containers that belong to Swarm services. |
enable |
Included | Included | Mixed mode. Use this when nginx-proxy should route both standalone containers and Swarm services. |
prefer-local |
Included | Included | Mixed Swarm mode that prefers healthy local task containers and keeps the service VIP as a fallback. |
strict |
Excluded | Included | Swarm-only mode. Use this when nginx-proxy should route only Swarm services. |
ignore is the default and does not require the Docker node to be in Swarm mode. In this mode, nginx-proxy only reads the normal Docker container API. If a Swarm task container is visible on the local Docker socket, it can be registered as if it were a regular container.
exclude still uses only the local Docker container API, but skips containers that have Swarm service labels. This is useful when the same Docker host runs standalone containers and Swarm services, but this proxy instance should only manage standalone containers.
enable reads both local containers and Swarm services. Standalone containers are discovered from the local Docker socket. Swarm services are discovered from the Swarm manager API, and task containers are skipped so each service is registered once.
prefer-local reads both local containers and Swarm services, but local Swarm task containers are also discovered from the local Docker socket. When a route has local containers and the Swarm service VIP, nginx sends normal traffic to the local containers and marks the service VIP as a backup upstream server. If no local container is available, the service VIP is used normally. Existing container healthcheck and BACKEND_START_GRACE_SECONDS behavior still applies before local containers are registered.
strict reads only Swarm services. Local standalone containers are ignored, and Swarm task containers are also ignored. This is the mode to use when this proxy instance is dedicated to Swarm routing.
For enable, prefer-local, and strict, the Swarm API client must be connected to a manager node because Docker only allows managers to list services. If nginx-proxy is running on a worker node, set SWARM_DOCKER_HOST to a reachable manager Docker API endpoint:
-e DOCKER_SWARM=enable \
-e SWARM_DOCKER_HOST=tcp://manager:2375If SWARM_DOCKER_HOST is not set, the local Docker socket is used for both local containers and Swarm services. When SWARM_DOCKER_HOST is set, nginx-proxy uses the local Docker socket for standalone containers and the remote manager socket for Swarm services. If the local Docker socket cannot be reached but SWARM_DOCKER_HOST is set, nginx-proxy switches to strict mode and uses only the remote Swarm manager.
Redirect traffic from one domain to another.
-e 'VIRTUAL_HOST=https://example.uk' \
-e 'PROXY_FULL_REDIRECT=example.com,www.example.uk -> example.uk'Set NGINX_STICKY_SESSION on every backend for a route that has multiple backends.
trueorip_hashenables simple client-IP affinity:docker run -e VIRTUAL_HOST=app.example.com -e NGINX_STICKY_SESSION=true ...
falsedisables affinity and uses round-robin balancing.- An advanced value is inserted as an Nginx upstream directive. For application-managed
cookies, use
hash $cookie_sessionid consistent; the application must set thesessionidcookie. In Compose, escape the variable asNGINX_STICKY_SESSION: "hash $$cookie_sessionid consistent".
All backends for the route must use the same value. ip_hash uses the client address
seen by Nginx, so clients behind the same NAT or proxy may share an affinity target.
In prefer-local mode, sticky balancing across multiple local tasks omits the
incompatible service-VIP backup. Docker events restore VIP routing when local tasks
disappear.
nginx-proxy automatically requests and renews Let's Encrypt certificates.
- Expose ports 80 and 443 on
nginx-proxy. - Map
/etc/nginx/ssland/etc/nginx/challenges. - Set
VIRTUAL_HOSTto start withhttps://in your containers.
nginx never waits for certificates. On startup and on every reload it serves every host with the certificate already on disk, expired ones included, and a new host is served self-signed while its certificate is requested. A host with neither is left out until one exists. Issuance and renewal run on a background thread and nginx is reloaded again once the new certificate files are in place.
To issue wildcard certificates (e.g., *.example.com) or avoid exposing port 80, you can use DNS challenges via Cloudflare.
- Set the
CLOUDFLARE_API_KEY*environment variables onnginx-proxy.nginx-proxysupports multiple api-keys and will discover available domains. - Set
VIRTUAL_HOSTto a wildcard domain (e.g.,*.example.com) or just use it for regular domains to use DNS validation.
Example:
docker run -d \
-e CLOUDFLARE_API_KEY_KEY1="your-cloudflare-api-token" \
...
mesudip/nginx-proxySet DEFAULT_SSL_DOMAINS to make nginx-proxy track and auto renew certificate for HTTPS domains that are not backed by a container.
-e DEFAULT_SSL_DOMAINS="*.example.com,*.example.net"These domains are rendered as normal hosted HTTPS sites, not as nginx default servers. If a container later configures the same domain and / route, the container route overrides the built-in default page.
Mount your own certificates:
- Directory:
/etc/nginx/ssl/certs - Naming:
example.com.crtandexample.com.key
Wildcard support: *.example.com.crt will match sub.example.com.
Use the getssl command inside the container:
docker exec nginx-proxy getssl example.comProtect your services with HTTP Basic Auth.
Set PROXY_BASIC_AUTH environment variable.
- Per-Host:
example.com -> user:password - Per-Path:
example.com/admin -> user:password
Note: Passwords should not be base64 encoded; the proxy handles hashing.
Requests with an unknown Host header return 503 Service Unavailable.
Control this with the DEFAULT_HOST variable on the nginx-proxy container:
true(default): Returns 503.false: Passes default requests to the first configured server.
To route unknown traffic to a specific container:
-e "VIRTUAL_HOST=fallback.example.com" \
-e "PROXY_DEFAULT_SERVER=true"docker exec nginx-proxy verify www.example.com ## check if request routes back to this server.
docker exec nginx-proxy getssl www.example.com example.com www2.example.com # issue certificate
docker exec nginx-proxy getssl --force www.example.com # skip the preliminary reachability check; ACME still validates
docker exec nginx-proxy reload # rescan Docker state and reload nginx config
verify checks every requested hostname by serving a temporary token and comparing the exact response from its HTTP port 80 challenge URL. With CERTAPI_URL, it calls CertAPI's /api/verify: the token is stored and checked by the CertAPI server through the public domain and nginx's challenge proxy. This requires CertAPI 1.1.17 or newer on both the proxy client and API server. Upgrade the CertAPI server before rebuilding/redeploying the proxy; older servers produce an explicit error. Verification does not request a certificate and does not use DNS-provider support as proof of HTTP routing. Wildcards cannot pass this HTTP check; wildcard issuance can still use a configured DNS solver.
verify exits nonzero if any domain fails and prints the cause it can find for each one (invalid hostname, DNS, connection refused, timeout, redirect); with CertAPI, the server's log has the rest. getssl uses the same certificate backend as automatic issuance and renewal, enables self-verification by default, and exits nonzero for incomplete issuance or a failed nginx reload. Remote certificates, including reused and wildcard certificates, are saved into the proxy's local SSL directory before reload. --force skips the preliminary check in both local and CertAPI modes; it does not skip ACME validation. Known issue: getssl reloads nginx without re-rendering the configuration, so a host still served with its self-signed certificate keeps it until the next Docker event; run docker exec nginx-proxy reload afterwards.
For local regression testing, tests/test_certapi_routing.py runs nginx and a temporary CertAPI service on loopback ports. It covers local challenges, HTTP and HTTPS CertAPI origins, runtime upstream routing, redirects, basic auth, unavailable backends, and certificate storage/reuse. Install nginx, pytest, flask, and flask-restx alongside the project dependencies to run it. Test certificates are signed by a temporary local CA.
We are constantly improving nginx-proxy to make it the most robust and versatile zero-config reverse proxy solution. Here’s what’s coming next:
- High Availability & Multi-Node Swarm Deployment: Comprehensive guides and templates for deploying
nginx-proxyin a multi-node Swarm environment for maximum uptime. - 100% Test Coverage: Ensuring rock-solid stability and reliability for every release.
- Remote Swarm Cluster Support: Monitor and route traffic for Swarm clusters running on remote nodes seamlessly.
- Pangolin Support: Integration with Pangolin as an alternative reverse-proxy engine.