Environment variables on Fly.io are more than just key-value pairs; they’re the primary mechanism for configuring your application at runtime without redeploying.

Let’s see how this plays out with a simple Go app. Imagine a web server that exposes its port via an environment variable.

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	port := "8080" // Default port
	if p := os.Getenv("PORT"); p != "" {
		port = p
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello from port %s!", port)
	})

	log.Printf("Listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

We can build this and deploy it with a default PORT of 8080:

fly launch
fly deploy

Now, to make this app listen on a different port, say 5000, we don’t touch the code. We just update the environment variables.

fly secrets set PORT=5000
fly deploy

After the deploy, if you check the logs (fly logs), you’ll see "Listening on port 5000". Visiting your app’s URL will now show "Hello from port 5000!". This dynamic configuration is a core tenet of cloud-native deployments.

The system solves the problem of immutable infrastructure. Instead of baking configuration into your Docker image or application code, you externalize it. This allows a single image to serve multiple environments (dev, staging, prod) or adapt to different network configurations without rebuilding. On Fly.io, environment variables are managed as "secrets," even if they aren’t sensitive, providing a unified interface for configuration.

Internally, when your Fly.io app starts, the fly-agent injects these secrets into the container’s environment. The operating system makes these variables available to your process, and your application code can read them using standard library functions like os.Getenv() in Go, process.env in Node.js, or os.environ in Python. The fly-agent fetches the secrets from Fly’s API on app startup and before restarts.

The exact levers you control are the fly secrets commands. fly secrets list shows what’s currently set. fly secrets set KEY=VALUE adds or updates a secret. fly secrets unset KEY removes one. Crucially, fly secrets set --app my-app KEY=VALUE targets a specific application, and fly secrets set --org my-org KEY=VALUE sets it for all apps in an organization. The deploy command (fly deploy) is what triggers the agent to pick up the new variables. If you forget to deploy after setting a secret, your app will continue to run with the old configuration.

One thing most people don’t know is how Fly.io handles secrets across different regions for a single application. When you set a secret for an application (fly secrets set MY_VAR=value), it’s replicated to all regions where that application is deployed. If your application has multiple instances running in different geographical locations, they will all receive the same secret value automatically. This global replication simplifies managing distributed applications, ensuring consistent configuration everywhere.

The next step is understanding how to manage secrets that are sensitive, like API keys or database passwords.

Want structured learning?

Take the full Fly-io course →