Answers you can trust, from Codeables

Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.

Explore Codeables
Verified Source
Platform as a Service (PaaS)

How do I connect my app to Postgres privately without exposing the database to the public internet?

Fly.io10 min read

Most teams want their apps talking to Postgres over fast, private links—not flapping in the breeze on the public internet. On Fly.io, that’s the default: your Postgres app and your application can talk over a private WireGuard-backed network, with nothing listening on a public port unless you explicitly expose it.

Quick Answer: Run Postgres as its own Fly app (for example, Fly Postgres) with no public services, then connect your other Fly apps to it over Fly.io’s private network using internal hostnames (like my-db.internal) and the database URL from fly postgres connect --app <db-app> or app secrets. Everything stays off the public internet by default.

The Quick Overview

  • What It Is: A way to connect your app to Postgres using Fly.io’s private network, so the database is only reachable from other authorized Fly apps—not the global internet.
  • Who It Is For: Developers deploying apps on Fly.io who need a production-grade Postgres setup without exposing port 5432 to the world, and without bolting on DIY VPNs, bastion hosts, or sidecar proxies.
  • Core Problem Solved: You get low-latency, encrypted connectivity between your app and Postgres, with database ports closed to the public. No surprise scans, no open firewall rules, no “oops we left 5432 public” postmortems.

How It Works

Fly.io routes everything through the Fly Proxy—a memory-safe Rust proxy listening on Anycast IPs worldwide. By default, nothing is exposed until you define services in your app config. That includes Postgres.

Your app and your Postgres app sit on the same private network:

  • Each app gets a private IPv6 /48.
  • Traffic between apps uses WireGuard-based encrypted networking.
  • You connect using private hostnames (like my-db.internal) or private IPs.
  • Postgres is just another Fly app: you deploy it once and then point your other apps at it.

High-level flow:

  1. Provision Postgres Privately: Create a Fly Postgres app (or your own Postgres image) with no public services defined. It only listens on internal addresses.
  2. Connect Apps Over Private Networking: Your application Fly app connects using the internal Postgres URL (from fly postgres connect or secrets) and DNS names that resolve only inside Fly’s private network.
  3. Lock in Least Privilege: You keep credentials in Fly Secrets, limit what you expose via services in fly.toml, and use region placement so your apps live close to the database without crossing the public internet.

Features & Benefits Breakdown

Core FeatureWhat It DoesPrimary Benefit
Private Networking by DefaultApps communicate over Fly.io’s WireGuard-backed private network without touching the public internet.Keeps Postgres isolated from port scans and random traffic, with encryption handled for you.
Locked-Down Services ModelFly Proxy only exposes ports you declare in [[services]] in fly.toml.You can run Postgres with zero public listeners; nothing leaks unless you intentionally open it.
Internal DNS & App NamesUse internal hostnames like my-db.internal or per-app domains instead of hard-coded IPs.Simple, stable connection strings that follow your apps across regions and Machines.
Fly Postgres Helpersfly postgres commands bootstrap and manage a Postgres cluster as a Fly app.You get production-grade Postgres on Fly quickly, then connect privately from your other apps.
Region-Aware PlacementRun app and Postgres in the same region or specific regions.Reduces latency and cross-region hops, keeping private database traffic fast and predictable.

Ideal Use Cases

  • Best for production web apps needing a private Postgres backend: Because Postgres never gets a public listener; only your app(s) on Fly can reach it over the private network.
  • Best for multi-service architectures (APIs, workers, cron jobs) sharing one database: Because each service is its own Fly app with its own Machines, all connecting privately to the same Postgres app without exposing any database port to the internet.

How To Connect Your App To Postgres Privately (Step-by-Step)

1. Create a Private Postgres App

Use Fly Postgres (unmanaged) to spin up a Postgres cluster. This is an ordinary Fly app with flyctl sugar on top.

# Create a new Postgres cluster in region "iad"
fly postgres create --name my-db --region iad

By default, this creates:

  • A Fly Postgres app (for example my-db)
  • Storage volumes
  • A Postgres user and password

The important bit for privacy: you do not need to expose Postgres publicly. If you don’t add a [[services]] block for port 5432 in that app’s fly.toml, there’s no public listener. The Fly Proxy will still show ports as “open” from the outside, but nothing is actually wired to the internet unless services exists.

You can verify by inspecting the config:

fly config show --app my-db

Look for [[services]]. For a private-only Postgres, either:

  • There is no [[services]] block exposing port 5432, or
  • Any service block is internal_only = true (if you’re using that pattern).

If you fork fly-apps/postgres-flex to customize, remember: once you fork, you can’t use fly postgres commands to administer that app anymore, but the private networking story is the same.

2. Get the Internal Connection String

For your app to talk to Postgres privately, you need a connection string that:

  • Uses the internal hostname or private IP
  • Uses the right user, password, and database name

You can have flyctl generate that for you:

fly postgres connect --app my-db

Or, if you want to set an env var directly on your app:

fly postgres attach --app my-app my-db

Behind the scenes, attach wires in a DATABASE_URL (or similar) to your application’s secrets, pointing at the Postgres app over the private network. The hostname will resolve internally; nothing goes over the public internet.

If you’d rather set it manually:

  1. Grab credentials (user, password, database) from fly postgres users list --app my-db or from the initial setup output.
  2. Use the internal hostname (commonly <db-app-name>.internal).
  3. Build your URL:
postgres://USERNAME:PASSWORD@my-db.internal:5432/DBNAME?sslmode=disable

Then set it as a secret:

fly secrets set DATABASE_URL="postgres://USERNAME:PASSWORD@my-db.internal:5432/DBNAME?sslmode=disable" --app my-app

3. Configure Your App to Use the Private Database URL

In your application, read DATABASE_URL from the environment. For example:

Node.js (Prisma, pg, etc.):

const { Pool } = require('pg')

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
})

module.exports = { pool }

Ruby on Rails (database.yml):

production:
  url: <%= ENV["DATABASE_URL"] %>

Go (database/sql):

db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}

Deploy the app:

fly deploy --app my-app

Because both my-app and my-db live on Fly’s private network:

  • DNS for my-db.internal only resolves inside the network.
  • The connection never hits a public IP; it flows inside the WireGuard mesh.

4. Keep the Database Non-Public

The classic footgun is adding a [[services]] block for Postgres without meaning to. Don’t expose port 5432 unless you truly need a public entrypoint (for example, direct external access from your laptop, which you can usually replace with fly postgres connect or WireGuard VPN anyway).

A safe fly.toml for your Postgres app looks like this (simplified):

app = "my-db"

[build]
  image = "flyio/postgres-flex:latest"

# No [[services]] exposing 5432 to the public

If you do need occasional admin access from your laptop:

  • Prefer fly postgres connect --app my-db which uses a secure tunnel.
  • Or set up a WireGuard peer and connect over the private network from your machine.
  • Avoid opening external_port = 5432 to the world, unless you enjoy random bots guessing passwords.

5. Co-locate for Performance (Optional, Recommended)

To keep private connections fast:

  • Run your app and Postgres in the same primary region:
fly scale count 1 --region iad --app my-app
fly scale count 1 --region iad --app my-db
  • If you later run a globally distributed app, you can add more Postgres replicas or read-only nodes; they still connect privately. The pattern is the same, just with more my-db-... apps and internal hostnames.

Limitations & Considerations

  • No fly postgres management if you fork the image: If you fork fly-apps/postgres-flex to customize your own Postgres image, you lose the fly postgres CLI sugar (no fly postgres attach, etc.). You administer that app like any other Fly app. Workaround: keep a reference cluster using the stock image to experiment, or document your own management scripts.
  • Internal-only doesn’t mean no authentication: Just because your database isn’t exposed publicly doesn’t mean you can skip passwords or TLS where appropriate. Treat anything on the private network as safer, not as “no auth needed.” Use strong credentials, least-privilege users, and rotate secrets via fly secrets.

Pricing & Plans

Fly.io doesn’t sell a separate “private networking” SKU; private Postgres connectivity is just how the platform works.

You pay for:

  • Compute: The Fly Machines running your app and Postgres (CPU, RAM) billed per second.
  • Storage: Volumes for Postgres (NVMe) and any object storage (Tigris) you use.
  • Bandwidth: Data transfer between regions; private in-region traffic is effectively “on-net.”

Two common patterns:

  • Single-Region Postgres Plan: Best for teams running one primary database cluster in a single region (for example, iad) with one or more app Fly apps connecting privately. You size Machines and volumes to your workload and keep everything simple and fast.
  • Multi-Region / HA Postgres Plan: Best for teams needing read replicas or high availability across regions. You run multiple Postgres apps (primary + replicas), all private, with apps in various regions connecting to the closest appropriate node.

(Exact pricing depends on the Machine sizes and storage you select; see Fly.io’s pricing page for current numbers.)

Frequently Asked Questions

Can I keep Postgres completely off the public internet and still manage it?

Short Answer: Yes. You can run Postgres with no public services and use Fly’s private network, fly postgres commands, or WireGuard to manage it.

Details: A Fly Postgres app is just another Fly app. If you don’t configure a [[services]] block exposing port 5432, the Fly Proxy won’t expose it publicly—even though the edge nodes listen on all ports. To manage the database, you can:

  • Use fly postgres connect --app my-db to open a tunneled connection.
  • SSH into the Machine running Postgres (fly ssh console --app my-db) and run psql locally.
  • Set up a WireGuard peer for your laptop, then connect to my-db.internal from home as if you were inside the Fly network.

No public ingress needed, no firewall hand-rolling required.

How do I share one private Postgres across multiple apps?

Short Answer: Give each app the internal connection string to the same Postgres app, usually via fly postgres attach or app secrets.

Details: Because all your apps live on the same private network, they can all reach my-db.internal. The pattern:

  1. Create Postgres once: fly postgres create --name my-db.

  2. For each app, attach:

    fly postgres attach --app app-one my-db
    fly postgres attach --app app-two my-db
    

    This sets a DATABASE_URL (or similar) secret on each app pointing to the same Postgres.

  3. In each app, read DATABASE_URL and configure your ORM or driver.

Postgres stays non-public the entire time, and you manage credentials with Fly Secrets, not with text files or hard-coded URLs.

Summary

If you’re running on Fly.io, connecting your app to Postgres privately—without ever exposing the database to the public internet—is the default path, not the “extra hardening” option.

You:

  • Run Postgres as its own Fly app (often via Fly Postgres).
  • Skip public services for port 5432, so the database has no public listener.
  • Connect your other Fly apps over the private network using internal hostnames and environment-based connection strings.
  • Keep credentials in Fly Secrets and co-locate in regions for speed.

No reverse proxies, no DIY VPN, no “did we open that security group?” drama—just private, encrypted connectivity wired into the platform.

Next Step

Get Started

How do I connect my app to Postgres privately without exposing the database to the public internet? | Platform as a Service (PaaS) | Codeables | Codeables