Fly.io Static Outbound IP: The Complete Fix (Including Deploys)

QuotaGuard Engineering
April 3, 2026
5 min read
Pattern

QuotaGuard gives your Fly.io app a fixed outbound IP address that works during deploys, migrations, and runtime traffic. Fly.io's community has documented this problem across multiple threads going back to 2022. The native add-on Fly shipped has two documented failure modes. This post covers all of it.

The Problem Has Three Chapters

If you've searched the Fly.io community for help with outbound IP addresses, you've landed in one of a few places.

The original thread from 2022 documents the core issue: Fly.io assigns your app a dynamic outbound IP, pulled from shared infrastructure. Every deployment, every restart, potentially every request can come from a different address. That breaks any third-party API, database, or enterprise system that uses IP allowlisting as a security control — banking APIs, payment processors, healthcare data vendors, and corporate internal systems all fall into this category.

A second thread followed asking when Fly would ship a native solution. Fly staff confirmed it was on the roadmap. The community waited. In the meantime, developers worked around it by spinning up dedicated VMs on other providers, paying for extra infrastructure, or holding off on integrations entirely.

Fly eventually shipped a static outbound IP add-on. A third thread appeared documenting what happened next: the native feature doesn't apply during deploys or migrations, and it adds meaningful latency to outbound traffic.

Three threads, the same underlying problem, documented over multiple years. If you're here because one of those threads didn't fully solve it, here's what actually works.

Why the Native Fly.io Static IP Add-On Falls Short

Fly's add-on is a network-level feature. It assigns a fixed IP to your app's egress traffic when the app is running normally. But when Fly executes your release command — your Ecto.Migrate step, your seed scripts, anything in the [deploy] section of your fly.toml — that process runs in a separate execution context. The static IP doesn't apply there.

The result: your app is running fine, requests go out from the fixed IP, but then you push a new version. The release phase tries to connect to an allowlisted database or API. The source IP doesn't match. The connection is rejected. Your deploy fails — not because of anything wrong with your code, but because of how Fly routes egress for build and release contexts versus runtime.

The second issue is performance. Developers who've measured this report 50–150ms of added latency when routing through Fly's static IP infrastructure. For a Phoenix app making frequent database round trips, that overhead compounds quickly.

Both of these are fundamental to how Fly implemented the feature, not configuration issues on your end.

The Proxy Approach: Works Everywhere

A static outbound proxy solves the deploy problem because it operates at the process level rather than the network interface level. Your application connects to the proxy. The proxy forwards traffic from its own fixed IP. This applies whether the process is your running app, a migration script, a release command, or a one-off task.

QuotaGuard provides exactly this. You get a proxy URL backed by two static IPs on AWS infrastructure. You configure the proxy in your app. Every outbound request that needs a fixed IP routes through it, from every execution context.

Setup on Fly.io

Set your proxy credentials as a Fly secret:

fly secrets set QUOTAGUARDSTATIC_URL="http://username:password@proxy.quotaguard.com:9293"

Python (requests)

import os
import requests

proxy_url = os.environ.get("QUOTAGUARDSTATIC_URL")

proxies = {
    "http": proxy_url,
    "https": proxy_url,
}

response = requests.get(
    "https://api.restricted-service.com/data",
    proxies=proxies,
    timeout=10,
)

print(response.json())

Node.js (axios)

npm install axios https-proxy-agent
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');

const agent = new HttpsProxyAgent(process.env.QUOTAGUARDSTATIC_URL);

const response = await axios.get('https://api.restricted-service.com/data', {
  httpsAgent: agent,
});

console.log(response.data);

Elixir / Phoenix (Req)

defmodule MyApp.HttpClient do
  def get(url) do
    proxy_url = System.get_env("QUOTAGUARDSTATIC_URL")
    {proxy_host, proxy_port, proxy_opts} = parse_proxy(proxy_url)

    Req.get!(url,
      connect_options: [
        proxy: {:http, proxy_host, proxy_port, proxy_opts}
      ]
    )
  end

  defp parse_proxy(url) do
    uri = URI.parse(url)
    [user, pass] = String.split(uri.userinfo || "", ":")
    {uri.host, uri.port, [proxy_auth: {user, pass}]}
  end
end

Fixing the Deploy Problem with QGTunnel

For TCP connections — Ecto connecting to an external Postgres or MySQL instance, for example — the HTTP proxy approach doesn't apply. Database connections aren't HTTP. For these, use QGTunnel, which wraps your outbound TCP connections transparently and routes them through the same static IP.

Add QGTunnel to your Dockerfile:

RUN curl -s https://s3.amazonaws.com/quotaguard/qgtunnel-latest.tar.gz | tar xz -C /app

ENV QGTUNNEL_DESTINATIONS="your-db-host.example.com:5432"

Then wrap your app process in fly.toml:

[processes]
  app = "/app/qgtunnel /app/bin/my_phoenix_app start"

Because QGTunnel runs inside your application process — not at the network interface level — it applies to every context your application runs in: normal runtime, release commands, and migrations. The deploy failure that Fly's native add-on produces doesn't happen here.

Selective Routing: Only Proxy What Needs It

Don't route all outbound traffic through the proxy. Only the requests going to allowlisted systems need a fixed IP. Everything else — internal service calls, database connections to your Fly Postgres — should go direct. Here's a clean pattern:

import os
import requests

PROXY_URL = os.environ.get("QUOTAGUARDSTATIC_URL")

ALLOWLISTED_HOSTS = {
    "api.restricted-service.com",
    "secure.vendor.io",
}

def get(url: str, **kwargs):
    host = url.split("/")[2]
    if host in ALLOWLISTED_HOSTS:
        kwargs.setdefault("proxies", {"http": PROXY_URL, "https": PROXY_URL})
    return requests.get(url, **kwargs)

What About Fly's Shared IP Ranges?

Some threads explored whether Fly could publish stable CIDR ranges per region so API providers could allowlist the whole range. This doesn't work in practice. Allowlisting a /16 that hundreds of other Fly customers share is a security tradeoff most enterprise teams won't accept. A bank, a government system, or a healthcare vendor will reject it outright. A dedicated static IP that only your app uses is what their security teams require.

When the Native Fly.io Add-On Is Still Useful

If your use case is latency-tolerant, you're not running migrations against an allowlisted host, and you're not on Phoenix making frequent database round trips — Fly's native add-on may be adequate. It's zero-code to configure and keeps you in the Fly ecosystem. For anything where the deploy failure mode or latency overhead is a real constraint, you need a different approach.

Cost Comparison

Fly.io's native static egress is $3.60/month per IPv4 address, with IPv6 included. You need one IP per region — so if your app runs in three regions, that's $10.80/month. If the native feature covers your use case and downtime isn't a critical concern, it's a reasonable starting point.

QuotaGuard's Micro plan is $19/month: two load-balanced static IPs, redundant failover, and the same IP works regardless of how many regions your app runs in. The price difference is small, but the more important distinction is what you're connecting to. If you're running a personal project or a to-do app and the occasional dropped connection is acceptable, Fly's native add-on is probably fine. If you're processing crypto transactions, connecting to a payment gateway, calling a financial API, or running anything where a failed outbound connection means a failed transaction or a missed critical event — that's a different category of requirement. Those apps need a proxy that's built for production reliability, maintained infrastructure, and has redundancy designed in from the start.

Some developers also consider spinning up a small VM on another provider and running Squid or HAProxy as a forward proxy. On paper it's cheaper. In practice, you're now the one responsible for uptime, security patches, certificate renewal, and what happens when the VM goes down at 2am during a high-traffic period. For an app where the proxy is in the critical path of real transactions, that's not a maintenance burden most teams want to own. Managed infrastructure exists precisely for this reason.

Solve It Today

The Fly.io community has been documenting this problem since 2022. The native add-on partially addresses it but introduces new failure modes for deploy-time connections and latency-sensitive apps. The proxy approach — one environment variable, requests routed through QuotaGuard, static IP registered in the allowlist — takes about ten minutes and works across all execution contexts on Fly.

See plans and pricing at quotaguard.com/products/pricing.

QuotaGuard Static IP Blog

Practical notes on routing cloud and AI traffic through Static IPs.

Reliability Engineered for the Modern Cloud

For over a decade, QuotaGuard has provided reliable, high-performance static IP and proxy solutions for cloud environments like Heroku, Kubernetes, and AWS.

Get the fixed identity and security your application needs today.