A Postgres inside a Docker container usually has no public port — good. This page is how to reach it anyway, without changing that.
The short version: never expose 5432 to the internet. Pick the row
that matches where the container runs; every path below works with FeatherDB as shipped.
| Where the container runs | The path | FeatherDB connection |
|---|---|---|
| Your own VPS (Coolify, plain Docker) | loopback port + SSH | SSH tunnel |
| Your own VPS, nothing published | the container, found by name + SSH | SSH tunnel → A container on the server |
| A PaaS with a CLI (Fly, Railway, GCP, AWS) | the provider's tunnel CLI | Direct → 127.0.0.1, TLS disable |
| Kubernetes | kubectl port-forward | Direct → 127.0.0.1, TLS disable |
| A managed database product | the provider's endpoint | Direct over TLS |
The container network is private and should stay that way. Publish the database port on the server's loopback only — it becomes reachable from the server itself and from nowhere else — then let SSH carry you there. In the service's compose file:
services:
postgres:
ports:
- "127.0.0.1:5433:5432" # loopback only — invisible to the internet
Redeploy, then in FeatherDB add an SSH tunnel connection: your server as the SSH
host, and 127.0.0.1 port 5433 as the database host as seen from
the server. Nothing new is exposed; the SSH key is the only door.
In Coolify specifically, avoid the database resource's “make it publicly available” switch — it binds the port on every interface, which is exactly the thing this page exists to avoid.
You can also skip the port mapping entirely. The server itself can reach every container
on its Docker networks, so an SSH tunnel can carry you straight to the container itself. In
FeatherDB, add an SSH tunnel connection, and on the database step choose A container
on the server: type the Compose service name (usually db), click Find it on
the server and pick your stack. FeatherDB looks up the container's current address every
time you connect, so a redeploy that moves it changes nothing for you. To do that lookup it
runs docker ps and docker inspect on the server over your SSH login —
nothing else, and only for connections where you chose it; the SSH user must be allowed to
use Docker (root, or a member of the docker group).
Older versions, or if you'd rather type the address yourself, find it on the server:
docker ps # the database container's name
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' <name>
Then use that address (for example 10.0.5.4) and port 5432 as the
database host. The trade-off: Docker hands out that address when the container starts, so a
redeploy can move it — if the connection stops answering after a deploy, look it up again, or
switch the connection to A container on the server.
No SSH access to the host — but every serious platform ships a tunnel in its CLI. It opens a port on your Mac and carries the traffic inside your authenticated platform session:
fly proxy 5433:5432 -a my-db # Fly.io
railway connect postgres # Railway
cloud-sql-proxy INSTANCE --port 5433 # Google Cloud SQL
aws ssm start-session … # AWS, via SSM port forwarding
Then add a Direct connection to host 127.0.0.1, the forwarded port,
and set TLS to disable (local proxy/tunnel only). That setting exists
for exactly this shape: the tunnel itself carries the encryption, the local leg is
plaintext, and a database that has no TLS configured would refuse an encrypted handshake
outright.
kubectl port-forward svc/postgres 5433:5432
Same as above: Direct → 127.0.0.1:5433, TLS disable. The
forward lives as long as the kubectl process does.
If you connect to the same stack every day, a mesh VPN (Tailscale, WireGuard) is the low-friction path: run its agent as a sidecar next to the database and the container gets a private IP only your machines can reach. Connect Direct to that IP — no public port, no CLI to keep running.
Supabase, Neon, RDS and friends hand you a public endpoint on purpose — TLS
required, strong auth, a pooler in front. That is not the risky kind of exposure; it is the
product. Use a Direct connection with TLS require, or
verify-full when the provider gives you a CA to pin.
No CLI tunnel, no sidecar, no managed endpoint — rare, but it happens. Then do it the
defensible way: TLS on the server (verify-full from FeatherDB),
scram-sha-256 auth, and a firewall allowlist holding exactly your IP. A bare
containerized Postgres published on 0.0.0.0:5432 with a password as its only
layer is the one configuration this page is written against.