Fly.io’s private networking, powered by WireGuard, isn’t just about connecting your apps; it’s a fundamentally different way to think about distributed systems.
Imagine you have two Fly.io apps, app-a and app-b, running in different regions, say ord and iad. Normally, to communicate, app-a would need to know app-b’s public IP address and port, and vice-versa. This means exposing your services to the internet, even for internal communication, which is a security and complexity nightmare. Fly.io’s private networking sidesteps this entirely. When you set up a private network, Fly.io essentially creates a virtual private network (VPN) that spans across all your deployed Fly.io apps, regardless of their region. Your apps can then talk to each other using their internal Fly.io-assigned IPs, as if they were on the same local network.
Let’s see this in action. Suppose we have a simple Go application running on Fly.io that acts as a basic API.
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080" // Default to 8080 if PORT env var is not set
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from %s!", os.Getenv("FLY_APP_NAME"))
})
log.Printf("Listening on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
We deploy this app to two different Fly.io organizations or projects, let’s call them api-service-east and api-service-west.
fly deploy --app api-service-east
fly deploy --app api-service-west
Now, we want api-service-east to be able to call api-service-west. Without private networking, api-service-east would need to know the public IP of api-service-west.
To enable private networking, we first need to create a WireGuard network.
flyctl wireguard create --name my-private-network
This command returns a WireGuard configuration. The crucial part is the Address field, which looks something like 10.1.2.3/24. This is the private IP range for your network.
Next, we attach our apps to this network.
flyctl wireguard attach --peer-name my-private-network --app api-service-east
flyctl wireguard attach --peer-name my-private-network --app api-service-west
After running these commands, Fly.io provisions WireGuard interfaces within your app’s VMs and establishes secure tunnels between them. Each app now has a private IP address within the 10.1.2.3/24 range. You can find these IPs by inspecting your app’s network configuration.
flyctl wireguard peers --app api-service-east
flyctl wireguard peers --app api-service-west
The output will show the private IP assigned to each app, for example:
For api-service-east:
Peer Name App Name Region Virtual IP
my-private-network api-service-east ord 10.1.2.4
For api-service-west:
Peer Name App Name Region Virtual IP
my-private-network api-service-west iad 10.1.2.5
Now, from api-service-east, we can make an HTTP request to api-service-west using its private IP:
# Inside a shell on api-service-east VM
curl http://10.1.2.5:8080
The output will be: Hello from api-service-west.
This demonstrates that api-service-east can communicate with api-service-west directly, without any public exposure, and with the security and low latency of a private connection. The magic here is that Fly.io manages the WireGuard key exchange, tunnel setup, and routing automatically. You don’t need to configure any complex firewall rules or NAT gateways. The FLY_APP_NAME environment variable is still available, but the communication is happening over the WireGuard private IP.
The core problem this solves is the inherent insecurity and operational overhead of exposing internal services to the public internet for inter-service communication. Instead of relying on DNS resolution of public IPs and managing TLS certificates for every internal hop, you leverage a flat, private IP space. This drastically simplifies service discovery and reduces the attack surface of your application infrastructure. The WireGuard configuration itself is managed by Fly.io, so you don’t have to worry about generating keys, distributing them, or maintaining the VPN infrastructure.
The one thing most people don’t realize is that the PORT environment variable your application listens on is still the same PORT it would listen on for public traffic. WireGuard acts as a layer below your application’s network stack. When traffic destined for 10.1.2.5:8080 arrives at the api-service-east VM, Fly.io’s networking layer intercepts it, encrypts it, routes it over the WireGuard tunnel to the iad region, and then decrypts it. The api-service-west VM then forwards this decrypted traffic to the application process listening on its PORT (which is also 8080 in this example). You don’t need to change your application’s port binding.
The next concept you’ll naturally explore is how to manage more complex service discovery within this private network, especially when you have many services and want to avoid hardcoding private IPs.