Nginx
nginx is the HTTP reverse proxy that fronts multiple web
services — dashboards, APIs, admin panels — with path-based routing and
optional TLS termination. pgcli runs it as a standalone, top-level addon with
the same CLI surface as the other web/infra addons (install / start / stop / logs
/ autostart / remove), managed entirely through pg.yaml.
It is the fleet’s HTTP gateway, which shapes its design:
- A reverse proxy, not a load balancer. Unlike HAProxy — a
TCP load balancer in front of Patroni — nginx is an HTTP reverse proxy that
routes by URL path. Each
locationblock forwards to one upstream (web app, API service, etc.), so clients reach multiple services through a single port. - File-driven configuration. pgcli renders
nginx.conffrom the backends you declare (or accepts a custom file via--conf-file) and bind-mounts it read-only into the officialnginx:1.27-alpineimage. TLS is optional: when enabled without a BYO cert, pgcli generates a self-signed leaf viatlsca. - Independent log mounting. Each backend gets its own access and error log
files (
access_<name>.log,error_<name>.log) under a bind-mounted/var/log/nginx/directory, so per-service traffic is observable without parsing a shared log. - Nginx-specific subcommands. Beyond the generic addon lifecycle
(install/start/stop/remove), nginx exposes
test(validate config syntax vianginx -tusing a temporary container),reload(hot reload vianginx -s reload), andexec(run arbitrary commands inside the container).
Platform support: nginx works on both Linux (host networking) and macOS (pgcli-net bridge with published ports). The upstream image is multi-arch (amd64 + arm64), so any host architecture works.
How It Works
Routing is path-based: each location block forwards to one upstream. The
config is rendered from the backends you declare (or copied from a custom
file), bind-mounted read-only, and nginx runs in the foreground with
nginx -g 'daemon off;'.
Two configuration modes
The mode is chosen with --conf-file (custom) or --upstream (template):
- Template mode (default) — pgcli renders
nginx.conffrom--upstreamspecs. Each upstream becomes anupstreamblock and alocationblock; TLS adds an HTTPS server block alongside the HTTP one. The template covers the common case: path-based routing to a handful of web services. - Custom mode (
--conf-file) — you provide the fullnginx.confpath; pgcli copies it into the addon directory and manages only the container lifecycle. Use this when production configs grow beyond the template’s reach (limit_req_zone,resolver+ variableproxy_pass, WebSocket, SNI, multiple locations per backend, upstream keepalive, etc.).--upstreamand--conf-fileare mutually exclusive.
Per-backend logs
Each backend gets its own log files in the bind-mounted /var/log/nginx/
directory:
access_<name>.log— access log for that backend’slocationblockerror_<name>.log— error log for that backend’slocationblock
The global access.log and error.log capture server-level events. The log
directory is created on the host at <base-dir>/addon/nginx/<name>/log/ and
persists across container restarts.
TLS termination
TLS is optional. When enabled without --tls-cert/--tls-key, pgcli generates
a self-signed leaf certificate via tlsca.Generate under
<base-dir>/addon/nginx/<name>/tls/. The HTTPS listener runs alongside the
HTTP one, so existing plain-text clients keep working. With BYO certs, the
given files are bind-mounted instead.
A BYO cert is often a pg cert leaf — that command mints a self-signed
certificate with any mix of DNS and IP SANs, exactly the shape a Git/DB
endpoint needs when clients you control can be handed the CA file:
Proxying Git (and any large POST): raise client_max_body_size. The
template mode sets no request-body limit, so nginx applies its built-in default
of 1 MiB — a git push (or any upload) larger than that comes back as
HTTP 413. The template gives you no flag for this, so front a Git service
with custom mode (--conf-file); see the complete working example under
“Git HTTP push with client_max_body_size” in Common custom config patterns
below.
Install
Backends are supplied one of two ways (mutually exclusive):
--upstream name=<n>,path=<p>,backend=<h:p>(repeatable) — explicit upstream targets. Repeatbackend=within the same--upstreamspec to add multiple servers (nginx round-robin), or use the shorthandbackends=h1:p1,h2:p2,....backendandbackendsare mutually exclusive within a single--upstreamspec;--conf-file <path>— a customnginx.conffile (skips template rendering entirely).
What each --upstream field means — for
--upstream name=web,path=/app,backend=127.0.0.1:8000:
| Field | Meaning |
|---|---|
name |
upstream name in nginx.conf (defaults to a sanitized path if omitted) |
path |
location path prefix (e.g. /app → web app, /api → API service; defaults to /) |
backend |
host:port to proxy to (e.g. 127.0.0.1:8000); repeat for multiple servers (round-robin) |
backends |
shorthand for multiple servers: backends=h1:p1,h2:p2,... (mutually exclusive with backend within one --upstream spec) |
The install summary reports the assigned ports and the config path:
With --conf-file, the summary says Config: <path> (from <confFile>) and
skips the Backends: section (pgcli does not parse the user’s config).
Rebuilding with --force
pg addon install nginx without --force on a running container now
auto-reloads config changes: it writes the new nginx.conf, validates it
with nginx -t, and sends nginx -s reload on success. If validation fails,
the old config is restored and the reload is skipped. This means most edits
(backends, --conf-file, worker_connections, TLS) apply without restarting
the container.
--force is still needed when the change requires a container recreate
(image tag, listen address, ports, networking mode): it stops the running
container, removes it, and creates a new one from the current config:
Nginx subcommands
Beyond the generic addon lifecycle (install/start/stop/remove), nginx exposes three subcommands for day-to-day operations:
test — validate config syntax
Runs nginx -t inside a temporary container (podman run --rm) with the
same mounts as the real one. Does not require the nginx container to be
running — it spins up a throwaway container, validates the config, and exits.
This is the safe way to test config changes before reloading:
--conf-file validates a standalone config file before installing it —
useful for CI pipelines or pre-deploy checks:
The temporary container mounts the same nginx.conf (and TLS cert directory
if TLS is enabled), so the validation is identical to what the real container
would see on startup.
reload — hot reload configuration
Sends nginx -s reload to the running container — a graceful restart that
picks up config changes without dropping in-flight connections. Requires the
container to be running (use pg addon start nginx --name proxy first if it
is stopped).
exec — run commands inside the container
Runs an arbitrary command inside the nginx container with stdio passthrough.
Requires the container to be running. The -- separator is mandatory —
everything after it is passed verbatim to the container.
Parameters
One container from the upstream docker.io/library/nginx image (pull-only —
pgcli never builds it), configured entirely through the rendered (or custom)
nginx.conf and the podman run flags. The table lists every knob and its
default:
Flag / pg.yaml key |
Default | Meaning |
|---|---|---|
--image / image_tag |
docker.io/library/nginx:1.27-alpine |
override the tag verbatim |
--http-port / http_port |
auto from nginx_start_port (base 8080) |
HTTP listener host port |
--https-port / https_port |
auto (next free port after HTTP) | HTTPS listener host port (TLS only) |
--listen / listen |
127.0.0.1 |
bind address (0.0.0.0 to expose) |
--worker-connections / worker_connections |
1024 |
worker_connections in the events block (max simultaneous connections per worker) |
--tls / tls |
false |
enable HTTPS listener alongside HTTP |
--tls-cert / tls_cert |
unset | BYO cert path (PEM leaf + chain); without it, pgcli generates a self-signed cert |
--tls-key / tls_key |
unset | BYO private key path (PEM) |
--upstream |
unset (repeatable) | name=<n>,path=<p>,backend=<h:p>[,backend=<h:p>...] — one upstream target, may include multiple backends |
--conf-file / conf_file |
unset | path to a custom nginx.conf (mutually exclusive with --upstream) |
| — | autostart: false |
start on boot (pg autostart enable --nginx) |
How the knobs combine:
--conf-fileis mutually exclusive with--upstream. Both mean “here is the config” — one by path, the other by declaring backends. The install rejects the combination.--tlswithout--tls-cert/--tls-keygenerates a self-signed cert. The cert lives under<base-dir>/addon/nginx/<name>/tls/and is bind-mounted at/etc/nginx/certs. The HTTPS listener runs alongside HTTP on the next free port.--tls-cert/--tls-keywithout--tlsis ignored. The flags only take effect when TLS is enabled.--worker-connectionsinjects into--conf-filemode. When both are set, pgcli patches the user’seventsblock (or adds one) to set the given value. The user’sworker_connectionsdirective, if any, is replaced — not merged.--listendefaults to loopback — the proxy stays on127.0.0.1unless you pass--listen 0.0.0.0to reach it from another host. On Linux the container shares the host network, so0.0.0.0answers on<host-ip>:<port>; on macOS the bridge publishes the port andproxyBindHostwidens loopback to0.0.0.0on the bridge.
Ports
Each instance takes one or two ports from nginx’s own pool,
nginx_start_port (default 8080) — a separate cursor from pgAdmin’s,
Redis’s, Predixy’s, and the object stores’ pools, so an nginx, a redis, and a
minio instance never collide:
--http-port and --https-port fix an instance to specific ports;
auto-assignment skips anything already explicit or live on the host. A TLS
instance consumes two ports (HTTP + HTTPS); a plain instance consumes one.
Configuration
Instances live under the top-level addons.nginx map in pg.yaml:
Edits to listen, http_port, https_port, worker_connections, tls,
tls_cert/tls_key, conf_file, or backends take effect on the next
pg addon install nginx --name proxy — config-only changes are auto-reloaded
(no --force needed). Use --force when the change requires a container
recreate (image tag, listen address, ports).
Common custom config patterns
When the template mode is not enough, use --conf-file with a full
nginx.conf. The following patterns are extracted from a real 70+ server
production config and cover the most common cases.
Complete http block skeleton — the patterns below show individual
server blocks; wrap them in an http block like this:
Dynamic DNS resolution with resolver + variable proxy_pass — avoids
nginx resolving the hostname at startup and caching it forever. The variable
forces a fresh lookup on every request:
Rate limiting per URL — limit_req_zone at the http level, applied per
location. Use different zones for different rate tiers:
WebSocket proxy — requires Upgrade and Connection headers plus
HTTP/1.1:
Upstream with keepalive — reduces connection overhead for high-throughput backends:
Host-based routing with proxy_set_header Host — proxy to a backend
by IP but set the correct virtual host:
SNI passthrough with proxy_ssl_server_name — required when the upstream
is behind HTTPS and uses SNI to select the certificate. Without it, nginx does
not send the Host header during the TLS handshake and the upstream may reject
the connection:
Per-location rate limiting in a single server — when multiple paths need
different rate limits, declare separate zones and apply them per location.
A catch-all limit_req at the server level provides a baseline; specific
paths override with tighter or looser limits:
Custom log format — replace the default combined format with one that
captures request body, response timing, and upstream latency. log_format is
declared once in the http block; each server sets its own access_log path.
The log directory /var/log/nginx/ is bind-mounted by pgcli — use it for all
custom log paths:
Git HTTP push with client_max_body_size — front a Git host (Gitea,
GitLab, a bare git http-backend) with a complete custom nginx.conf that
lifts the 1 MiB body limit and keeps long git-receive-pack requests alive:
--conf-file and --upstream are mutually exclusive, but --tls /
--tls-cert / --tls-key still apply in custom mode: pgcli mounts the cert
pair at /etc/nginx/certs/public.crt and /etc/nginx/certs/private.key, which
the config above references — a custom file simply takes over writing the
listen/ssl_certificate lines itself.
Static file serving with caching — serve files from a host-mounted directory with browser caching headers, sendfile optimization, and access control:
List
Start and stop
start only starts an existing container (and self-heals an improper state by
recreating it from the nginx.conf already on disk). The config and log
directories are preserved across stop/start.
Auto-start on Boot
Containers carry a --restart unless-stopped policy (crashes, not reboots). To
bring instances up after a host reboot:
nginx autostart is start-only (install the instance first). On boot it
reads the nginx.conf already on disk, so the proxy comes up with the last
known config. nginx is independent of the PostgreSQL stack — it is a reverse
proxy that dials its backends itself — so there is no ordering constraint and
boot-time pg start --autostart starts it alongside the other non-database
addons.
Remove
Stops and removes the container and deletes its config directory (including TLS certs and logs). An empty per-instance parent directory is pruned.
Logs
The container logs to stdout, so access and error events show up here. For per-backend logs, look at the bind-mounted log directory:
Troubleshooting
- Config syntax error on startup. The container exits and the restart
policy spins it. Check with
pg addon nginx test --name proxy(works even when the container is stopped) — it runsnginx -tin a temporary container and reports the exact line number. --conf-fileand--upstreamtogether. The install rejects the combination — pick one mode.- Can’t reach the proxy from another machine.
listendefaults to127.0.0.1. Reinstall--listen 0.0.0.0 --forceand accept that the proxy is then the only gate in front of the backends. - Image not found at install.
docker.io/library/nginxis pulled on demand; on an air-gapped host,podman loadthe tar first (catalog and export steps indocs/images.mdin the repo) and the pull is skipped. - Reload fails with “container not running”.
pg addon nginx reloadrequires the container to be up. Start it first withpg addon start nginx --name proxy.
Known limitations
- No load balancing or health checks. nginx here is a reverse proxy (path-based routing to a single upstream per location), not a TCP load balancer. For load balancing in front of Patroni, use HAProxy.
- Template is simple by design. The rendered
nginx.confcovers path-based routing to a handful of backends with optional TLS. Production configs that needlimit_req_zone,resolver+ variableproxy_pass, WebSocket, SNI, multiple locations per backend, or upstream keepalive should use--conf-fileinstead. - macOS is code-complete, lightly tested. The bridge path (published ports,
proxyBindHostwidening loopback to0.0.0.0on the bridge) mirrors the other bridge addons; it has been exercised but not yet under load.