Reverse proxy
Status: available, opt-in. sol2docker can run a managed reverse proxy so you can put a domain in front of a service or container — with automatic TLS, WebSocket/TCP/UDP support, client certificates, and behavioral blocking — all from the Reverse proxy page. Nothing about a specific site is hand-edited into an nginx file; you describe routes in the UI and the proxy applies them live, with zero downtime.
The proxy is a separate OpenResty image, shipped as
ghcr.io/sol2docker/proxy. It
is a dumb data plane: it pulls a versioned config bundle from sol2docker over an
outbound-only, per-environment token and serves it. All the product logic — what routes exist,
which certificates, ACME, bans — lives in sol2docker. The proxy token is data-plane only: it can
pull that one environment's config and report bans, and can never drive Docker.
Why a managed proxy
Exposing a service normally means hand-writing nginx/Traefik config outside the product, managing certificates by hand, and reloading with a blip of downtime. The managed proxy folds all of that into sol2docker:
- Domain → service/container mapping from the UI, on both standalone daemons and Swarm.
- TLS handled for you — issue and auto-renew via Let's Encrypt, or upload your own.
- Zero-downtime changes — a route, certificate, or ban edit hot-applies on the proxy's next poll; the proxy is never reloaded and never blanks.
- Survives restarts — the last-known-good config is persisted to a volume, so a restarted proxy serves immediately, before it has re-synced.
Deploy the proxy
From the Reverse proxy page, choose Deploy proxy. sol2docker holds the Docker socket, so it does the setup for you:
- Creates the shared network
sol2docker-proxy(an attachable overlay on Swarm, a bridge on a standalone daemon). Attach the services/containers you want to route to this network. - Checks that ports 80 and 443 are free on the target (a preflight; it refuses with a clear error, naming the conflicting service/container, rather than failing to bind).
- Deploys the proxy as stack
sol2docker(so the service issol2docker_proxy), mounting a named volume for its persisted state.
| Surface | How it runs | Why |
|---|---|---|
| Swarm | Host-mode, global (one replica per node) on :80/:443. | Host mode preserves the real client IP (so blocking works) and any replica can answer an ACME challenge. |
| Standalone | Published 80:80 / 443:443, restart: unless-stopped. | A single daemon terminating traffic directly. |
You can also copy the generated compose and deploy it yourself — the modal shows it. Either way the
service is labeled sol2docker.role=proxy, so it's badged Proxy wherever it appears in the
Stacks/Services/Containers lists (it stays fully manageable from the Reverse proxy → Deployment
tab too).
Routing
Add routes from the Routes tab (L7) — or the deployment's Routes. A route forwards matched
traffic to a target, which can be a Swarm service, a container, or a plain address;
the target must share the sol2docker-proxy network with the proxy.
| Scheme | Layer | Matches on | Notes |
|---|---|---|---|
| HTTP / HTTPS | L7 | Hostname + path prefix | Virtual-host routing; optional path stripping. |
| WebSocket | L7 | Hostname + path prefix | Upgrade handshakes are passed through (can be disabled per route). |
| TCP / UDP | L4 | A published listen port | Raw stream pass-through to the target port. |
Service and container targets are addressed by name (resolved through Docker's embedded DNS), so a Swarm VIP moving or a container restarting doesn't strand the route. Multiple backends are load- balanced with passive health — a failing backend is skipped until it recovers.
TLS
Set a route's TLS mode when you create it, and manage certificates from the Certificates tab.
| Mode | What it does |
|---|---|
| None | Plain HTTP. |
| Uploaded (bring-your-own) | Paste a certificate + chain + private key. The key is encrypted at rest and only ever decrypted into the config the proxy pulls. |
| Let's Encrypt | sol2docker requests and auto-renews the certificate. |
For Let's Encrypt, pick the challenge type:
- HTTP-01 — needs the proxy reachable on
:80for the domains. Works across multiple Swarm replicas; issuance waits until every live replica is serving the challenge. - DNS-01 — works behind a firewall and for wildcards. Either add a DNS provider (currently Cloudflare) on the DNS providers tab for automatic TXT records — its credentials are encrypted at rest — or choose manual and add the TXT record yourself when prompted.
A staging Let's Encrypt option is available for testing without burning production rate limits.
Until a managed certificate exists, :443 answers with a self-signed bootstrap certificate so the
handshake never hangs.
Mutual TLS (client certificates)
Any L7 route can require client certificates: set mTLS to optional (verify if presented) or
required, and provide the CA (PEM) that signs the allowed client certs. Requests without a valid
client certificate are rejected with 403.
Per-route options
Each L7 route has Behavior and Headers sections. All of these hot-apply — no redeploy.
| Option | Effect |
|---|---|
| Force HTTPS | Redirects http:// → https://. |
| HSTS | Sends Strict-Transport-Security on HTTPS responses (with a configurable max-age). |
| WebSockets | Allow or reject Upgrade handshakes for this route. |
| Connect / read timeouts | Per-route upstream timeouts, in seconds. |
| Request headers | Extra headers added to the upstream request. |
| Response headers | Extra headers added to the response returned to the client. |
Blocking & security
The proxy blocks abusive clients in-process (no NET_ADMIN, no fail2ban), and sol2docker
persists the decisions so they fan out to every replica. Precedence is allow > deny > ban.
- Allow / deny rules (the Allow/deny tab) — IPv4 or IPv6 CIDRs. An allow-listed client is never auto-banned; a denied CIDR is blocked immediately.
- Bans (the Blocked tab) — manual bans, plus automatic bans the proxy reports when a client trips the thresholds. Bans are audited and raise a notification.
- Rate limiting & auto-ban thresholds are configurable, with visible defaults, and update live without redeploying the proxy:
| Setting | Default | Meaning |
|---|---|---|
| Auto-ban | on | Automatically block clients that exceed the thresholds below. |
| Max requests / window | 300 / 10s | Request-rate ceiling per client. |
| Max errors / window | 25 / 60s | 4xx/5xx ceiling per client. |
| Ban duration | 3600s | How long an auto-ban lasts. |
On Swarm, run the proxy in host mode (the default deploy) so the real client IP is preserved — in routing-mesh mode the source IP is hidden and source-IP blocking can't work.
Managing it in the UI
The Reverse proxy page is organized into tabs, and every table is searchable and filterable:
- Routes — your L7 routes.
- Deployment — the proxy's stack → services → tasks (Swarm) or containers (standalone) in a hierarchy, with the normal service/task actions; click through to any detail page.
- Certificates — uploaded and Let's Encrypt certs, with expiry and renew-now.
- DNS providers — credentials for DNS-01.
- Blocked — active bans.
- Allow/deny — CIDR allow/deny rules.
How it works
sol2docker is the control plane; the proxy is a pull-based data plane — the same outbound-only, token-per-environment pattern as the node agent.
- sol2docker computes one config bundle per environment (routes, certificates, L4 streams, ACME challenges, bans, allow/deny rules, security thresholds) and bumps a per-environment version on any change.
- The proxy long-polls
GET /api/v2/proxy/config; on a change it applies the new bundle via an atomic generation-pointer swap — new config is staged, then a single pointer flip makes it live, so a request never sees a torn state and no reload happens. - A malformed bundle is never applied — the proxy keeps serving the last good one and logs it.
- Certificate private keys are decrypted only into the bundle served over the token-authenticated channel — never into an API response, log, or the audit trail.
Notes & limits
- Allow/deny rules and bans match both IPv4 and IPv6 CIDRs and fan out to every replica. The
proxy adds IPv6
:80/:443listeners automatically when the host kernel has IPv6 (so a v6 client can reach it and be blocked cluster-wide); on a host with IPv6 disabled, only IPv4 listeners are created. v6 clients still only arrive if the Docker network the proxy runs on has IPv6 enabled. - L4 (TCP/UDP) streams are pass-through: the per-route L7 options (force-HTTPS, HSTS, headers, timeouts, per-route blocking) apply to L7 routes only.
- The proxy needs to own
:80/:443on the node(s) it runs on — put other workloads behind it rather than next to it.