External database access
Expose managed databases to applications outside your private network through the Fleetdock Gateway — HAProxy TCP proxy, CIDR allowlists, and application credentials.
Fleetdock can expose managed databases to applications outside the private network through a Fleetdock Gateway (HAProxy). The control plane generates configuration from persisted endpoint state; HAProxy proxies TCP traffic with one public port per database.
The API never proxies database connections. Desired state lives in database_endpoints; HAProxy
config is derived and reconciled by the control-plane worker.
Architecture
External app → gateway.example.com:15432 (TCP) → HAProxy → server.address:instance.port → DatabaseThe gateway is colocated with the Manager in the Docker Compose stack. The control plane
writes haproxy.cfg and performs a hitless reload through the HAProxy master socket. A
config HAProxy rejects is rolled back automatically — the running workers keep serving the
previous version. Reloads only happen when the generated config actually changes.
The gateway container must have a network route to server.address:instance.port. If it
cannot reach the database, the endpoint reports error with the unreachable address.
Enable the gateway
External access is off by default — turning it on publishes a 50-port range on the host, which most installs never need.
fleetdock gateway enableThat flips FLEETDOCK_GATEWAY_ENABLED, activates the gateway compose profile (HAProxy and
its one-shot init, sharing /var/lib/fleetdock/gateway with the control plane), and prints
the port range to open in your firewall. fleetdock gateway disable reverses it.
By hand, the equivalent is FLEETDOCK_GATEWAY_ENABLED=true in .env plus
docker compose --profile gateway up -d.
Hostname
FLEETDOCK_GATEWAY_PUBLIC_HOST defaults to your main FLEETDOCK_DOMAIN. The gateway speaks
raw TCP on its own ports — no vhost, no SNI, no TLS termination — so it needs no DNS record
of its own.
Set it separately when the main record cannot carry raw TCP. The usual culprit
is a CDN-proxied record — Cloudflare's orange cloud forwards 80/443 only,
so every database endpoint on that name is unreachable. Point a grey-clouded
gw.example.com at the host and set the variable to it.
The hostname is copied into each endpoint when public access is enabled, so changing it
later does not rewrite connection URLs already issued. fleetdock domain explains how to
adopt a new one.
Port range and options
FLEETDOCK_GATEWAY_PORT_RANGE_START=15432
FLEETDOCK_GATEWAY_PORT_RANGE_END=15481The published port range and the allocator range come from the same variables. Widen one and you must widen the other, or endpoints are assigned ports that nothing forwards — a failure only external clients can see.
Optional:
FLEETDOCK_GATEWAY_DIAG_PORT=15431 # source-IP diagnostic; 0 disables it
FLEETDOCK_GATEWAY_SOURCE_IP_MODE=direct # or proxy-protocol, behind an L4 load balancer
FLEETDOCK_GATEWAY_CONFIG_PATH=/var/lib/fleetdock/gateway/haproxy.cfg
FLEETDOCK_GATEWAY_MASTER_SOCKET=/var/lib/fleetdock/gateway/haproxy-master.sock
FLEETDOCK_GATEWAY_ADMIN_SOCKET=/var/lib/fleetdock/gateway/haproxy-admin.sockEnable public access per database
On the database page → Connectivity → Enable public access with allowed CIDRs. Or use the API:
POST /v1/databases/{id}/public-access— enable with CIDR allowlistPATCH /v1/databases/{id}/public-access— update CIDRsDELETE /v1/databases/{id}/public-access— disable
Check status with GET /v1/databases/{id}/connectivity.
Private vs public endpoints
| Path | When to use | Endpoint |
|---|---|---|
| Private | Apps on the same private network as the database server | server.address:instance.port (internal) |
| Public | Apps outside the private network | FLEETDOCK_GATEWAY_PUBLIC_HOST:assigned-port (gateway) |
Public access is disabled by default. When enabled, the gateway hostname and port stay stable across migrations — only the private backend target changes. See Migration.
Application credentials
Create scoped credentials under Application credentials on the database page (or via
POST /v1/databases/{id}/credentials). Passwords are encrypted at rest; the connection URL
is shown once on create or rotate.
Permission profiles:
| Profile | Scope |
|---|---|
readonly | SELECT on the database |
readwrite | SELECT, INSERT, UPDATE, DELETE |
admin | Full database-scoped privileges |
Instance root credentials are never exposed for public use. Create application credentials with least-privilege profiles for external applications.
Rotate or revoke credentials via:
POST /v1/databases/{id}/credentials/{credentialId}/rotateDELETE /v1/databases/{id}/credentials/{credentialId}
Source IP fidelity
Allowlists match the source address HAProxy observes, which is not always your client's address. Getting this wrong rejects every connection, and the rejection looks like the server closing the connection immediately.
| Deployment | Address HAProxy sees | Allowlists work? |
|---|---|---|
| Linux host, published ports (iptables DNAT) | the real client address | Yes |
| Docker Desktop (macOS/Windows) | the Docker VM's NAT address | No — every real client is rejected |
| Behind an L4 load balancer | the balancer's address | Only with FLEETDOCK_GATEWAY_SOURCE_IP_MODE=proxy-protocol |
network_mode: host on Linux | the real client address | Yes, and avoids publishing a large port range |
Find out what your gateway actually sees. Run this from the machine that will connect:
curl http://gateway.example.com:15431It prints the address to allowlist.
A bare address is accepted and treated as a single host (/32 or /128). A CIDR with host
bits set — 10.1.2.3/24 — is rejected rather than silently widened to a network you did not
ask for.
Changing the allowlist
Edit it in place from the connectivity panel, or with PATCH /v1/databases/{id}/public-access.
The assigned port is preserved, so existing clients keep working. Disabling and re-enabling
allocates a different port and breaks every connection URL already handed out.
If the panel reports rejected connections with no successful sessions, the allowlist does not match where clients are arriving from — check the diagnostic port above.
What Fleetdock can and cannot verify
| Fact | How it is determined |
|---|---|
| The route is programmed and the gateway can reach the database | HAProxy's own health check, read over the stats socket |
| The database accepts TLS | The control plane dials the database directly and runs a real protocol probe |
| The published port is reachable from the internet | Not determinable — firewalls, NAT, and DNS sit outside the control plane |
Endpoint status reflects those facts:
| Status | Meaning |
|---|---|
pending | Not yet in the applied gateway config, or the last apply failed — see last_error |
active | In the applied config, and HAProxy reports the database reachable |
error | In the applied config, but the database is down. The listener stays up so a restart does not reallocate the port. |
disabling / disabled | Being removed / removed |
TLS
HAProxy uses TCP passthrough — it does not terminate TLS at the gateway. Clients negotiate
TLS with the database directly, so the database must support it for sslmode=require to work.
tls_status is measured against the database itself: a Postgres SSLRequest probe, or the
CLIENT_SSL capability bit in the MySQL/MariaDB handshake.
When tls_status is unsupported while tls_mode is required, issued connection URLs
carry sslmode=require and will fail until TLS is enabled on the database. Fleetdock does
not quietly downgrade the requirement, because that would drop encryption you asked for.
Security
- Public access is disabled by default
- CIDR allowlists are enforced in HAProxy (
tcp-request connection reject unless …) - Explicit
0.0.0.0/0is required for allow-from-anywhere — the UI warns when you choose it - Instance root credentials are never exposed for public use
- Connectivity and credential changes are written to an append-only
audit_eventstable (not yet exposed through the API or dashboard — query it withfleetdock psql)
See Security for the full trust model.
Production
Open the gateway port range in the host firewall. The control plane cannot verify
reachability from outside — firewalls, NAT and DNS sit beyond it — so this is the step most
often missed. The dashboard and API stay on 443; gateway ports are separate TCP listeners
on the same host.
On Linux, network_mode: host for the gateway service both avoids publishing a large port
range and preserves real client addresses for allowlists.
Troubleshooting
| Symptom | Check |
|---|---|
Endpoint stuck pending | last_error on the endpoint; the reconcile_gateway operation; whether the gateway container is healthy |
Endpoint shows error | last_error names the unreachable host:port — can the gateway container reach it? |
| Connection closes immediately | The allowlist. Run curl http://<public-host>:15431 from the connecting machine and compare. The panel also reports rejected-connection counts. |
server does not support SSL, but SSL was required | The database does not accept TLS — enable it, or recreate the endpoint with tls_mode=preferred |
| Cannot connect externally | DNS to FLEETDOCK_GATEWAY_PUBLIC_HOST, host firewall on the port range, and that the range is actually published |
| Connection refused | Gateway service running; port assigned within PORT_RANGE_START–END |
See also Troubleshooting — Gateway.
MVP limitations
- No TLS termination at gateway (passthrough only)
- One public port per database (no hostname/SNI routing)
- The gateway must have a network route to
server.address:port(same as DB admin today) - CIDR allowlists depend on the gateway observing real client addresses — see Source IP fidelity above
API reference
Gateway and credential endpoints are documented under the Databases tag in the API reference.
Networking
How the Agent and Manager communicate, which ports and directions matter, and how to reach the Manager from VMs like Multipass during local development.
Security
Fleetdock's trust model — Manager and Agent privileges, tokens and session invalidation, encryption, SSRF protection, compromise scenarios, and current limitations.