Serving static assets directly from your Fly.io application without a CDN is surprisingly efficient and often cheaper for many use cases.
Here’s a standard Nginx configuration that will serve your static files directly from your Fly.io instance:
worker_processes auto;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
server {
listen 80;
server_name _; # Catch all
root /app/public; # Assuming your static assets are in /app/public
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Optional: Cache static assets aggressively
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
}
This Nginx configuration, when deployed on Fly.io, treats your application server as the origin for your static assets. When a request comes in for a file (e.g., /styles.css), Nginx checks if the file exists in the root directory (/app/public in this case). If it does, Nginx serves it directly. If not, it proceeds to the next location block or returns a 404.
The try_files $uri $uri/ =404; directive is crucial. It first looks for a file matching the request URI (e.g., /images/logo.png). If it doesn’t find an exact file match, it tries to see if the URI corresponds to a directory and looks for an index.html or index.htm within it. If neither works, it returns a 404 Not Found.
The optional location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ block adds aggressive caching headers for common static asset file extensions. expires 1y; tells the browser to cache these files for a year, and add_header Cache-Control "public, immutable"; further instructs the browser and any intermediate caches that the content will not change. access_log off; reduces logging for these static assets, which can be a significant performance boost.
To implement this, you would typically:
- Place your static assets in a directory like
public/within your application’s source code. - Configure your application’s build process to copy these assets into the final image, usually to a location like
/app/public. - Include an Nginx configuration file (like the one above) in your Fly.io application’s deployment. You’ll need to ensure Nginx is installed and running as your web server. Many frameworks (like Rails or Node.js with Express) have ways to integrate or replace their default web server with Nginx. For a simple static site, you might use a minimal Dockerfile that just runs Nginx.
- Deploy to Fly.io.
The surprising truth is that for many applications, especially those with moderate traffic or assets that don’t change frequently, bypassing a CDN and serving directly from Fly.io’s globally distributed edge network can be faster due to reduced latency. Your Fly.io app server is already running on an edge location close to your user, and Nginx is highly optimized for serving static files.
Consider a simple Go application with a public directory:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// Serve static files from the "public" directory
fs := http.FileServer(http.Dir("public"))
http.Handle("/", fs) // This will serve index.html for the root path if it exists
port := "8080" // Fly.io's default port is 80, but we'll use 8080 internally and let the proxy handle it.
fmt.Printf("Starting server on port %s\n", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
And a fly.toml:
app = "my-static-app"
primary_region = "iad"
[build]
builder = "paketobuildpacks/builder:base"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
min_machines_running = 1
[[services]]
protocol = "tcp"
ports = [80, 443]
In this Go example, http.FileServer(http.Dir("public")) handles serving files from the public directory. When deployed, Fly.io’s edge proxy listens on ports 80/443, forwards requests to your internal_port (8080 here), and your Go app serves the files. This is essentially the same principle as the Nginx example, just implemented in Go.
The most counterintuitive aspect is how well this scales. Fly.io’s infrastructure is designed for this. Each Fly.io region has its own compute resources, and your app runs on a machine in the region closest to the user making the request. This means requests for static assets are often served from a data center that’s geographically very close, potentially closer than many traditional CDNs. The fly.toml configuration ensures your app runs in the closest region to the user automatically.
The one thing most people don’t realize is how much of the "CDN magic" is simply about having geographically distributed servers and efficient caching. Fly.io provides both. Your application code, or a lightweight server like Nginx, becomes the origin, but because it’s already at the edge, the "last mile" is incredibly short. You’re leveraging the same global network that makes Fly.io fast for dynamic applications.
The next step would be to explore how to integrate this with a dynamic backend on the same Fly.io instance, routing static assets directly to Nginx while dynamic requests go to your application code.