I recently joined the team at Fly.io, and have slowly been moving my various sites and hobby projects over. In case you haven’t heard of Fly.io, we help developers host apps (all sorts of apps, not only JavaScript-based ones) close to users – no devops skills required. I use Fly.io to host a few Python apps like this one and this one.

Before discovering Fly.io, distributed app hosting was primarily accessible to JavaScript developers. Platforms like Vercel, Netlify, and Cloudflare Sites made it super easy to host sites and apps written in JavaScript. As someone who primarily works in Python, I always ended up deploying to Heroku or Render and caching static assets close to users. The problem is my Python apps use Python because Python is needed to execute the application logic. For most of my use cases, caching static assets on the edge doesn’t result in much of a performance boost. The REAL performance gains happen when the application logic can also execute on the edge – and that’s what Fly.io makes possible, and easy.

But this post is not about Python apps, it’s about static sites. I just wanted to set the scene with the Python example to demonstrate the value proposition of Fly.io for dynamic sites. But do the advantages and benefits carry over to the world of static site hosting?

At first, I thought the answer to that question was “no”. In fact, when I joined Fly.io, I had no intention of moving this site – a static site built with Hugo – to Fly.io. After all, there are a ton of polished and purpose-built static site hosting platforms out there, so why reinvent the wheel on Fly.io?

Last July, I wrote about migrating from Vercel to Cloudflare Pages and leveraging Cloudflare’s image resizing service for processing my images. At some point, Cloudflare’s image resizing service just got too expensive for my needs. I upload a ton of photos to this site, and use srcset to deliver responsive images to visitors – this translates to a lot of unique resizing requests and a $50/month Cloudflare bill (this includes the $29 Pro Plan). For me, $50/month is hard to justify because I don’t make much money from this site, and making money is not a long-term goal for this site.

At the same time, serving resized and optimized images to visitors is something that’s very important to me. I love fast websites, and I do everything I can to make sure this site loads as quickly as possible. I briefly considered going with Hugo’s built-in image resizing, but that didn’t end up working well because I have too many images and too many image sizes.

I ended up deploying imgproxy to Fly.io after comparing a few other Go-based options. I have no idea if it’s “the best option”, but it’s working really well for me. The initial setup consisted of several Fly.io VMs (I chose NRT, SYD, ORD, and AMS for decent global coverage) running imgproxy listening for requests to images.brianli.com.

With this setup, a request to…

https://images.brianli.com/unsafe/resize:fill:1920:0/plain/https://brianli.com/image.jpg

…would instruct imgproxy to download the JPG image at https://brianli.com/image.jpg, resize it 1920px wide, and serve it.

This worked great, but the URL structure looked so ugly to me.

I know, I know. There are so many other things to worry about in life. I don’t know what to say. I really couldn’t stop thinking about this ugly and overly complicated URL structure even though realistically, no one would ever see it… and even if someone did see it, they probably wouldn’t care.

But I care, and this is my site. So, I suppose I have a responsibility to make my image URLs look decent and presentable.

One way to solve this problem is to use a reverse proxy. At this point, my site was still hosted on Cloudflare Pages. I thought about implementing the reverse proxy with a Cloudflare Worker, but I have an “avoid JavaScript if at all possible” rule that I follow religiously.

So, I thought about the situation for a little while… How do I keep my site distributed around the world, while solving this ugly image URL problem at the same time? The answer is let’s host everything on Fly.io!

Next, I made a multi-stage Dockerfile:

  • Stage 1 – use the official Node image to build my site. I chose the Node image because I use TailwindCSS and need npm to build CSS files.
  • Stage 2 – use the official Caddy (a very easy to use web server) image to serve the static assets generated in Stage 1.

With this architecture, I control the server, which means I have ultimate flexibility when it comes to how my site is served, and pricing isn’t determined by a per request model like Cloudflare Workers which sort of can act like a server because it can intercept and manipulate requests.

I set up a path handler for image resizing requests like so:

handle_path /i/* {
	@imgresize {
		path_regexp imgresize \/(resize:fill:\d+:\d+)\/(.*\.jpg|png)$
	}
	rewrite @imgresize /unsafe/{re.imgresize.1}/plain/https://brianli.com/{re.imgresize.2}
	reverse_proxy http://images-brianli-com.internal:8080
}

P.S. I have no idea if this is the “best way” or the “right way” to do this in Caddy. I just picked Caddy up earlier today, which I guess is a testament to the quality of their documentation.

With this rule in place…

https://images.brianli.com/unsafe/resize:fill:1920:0/plain/https://brianli.com/image.jpg

becomes…

https://brianli.com/i/resize:fill:1920:0/image.jpg

Much better!

Also, notice how the reverse proxy target is http://images-brianli-com.internal:8080. This is a Fly.io internal hostname, which means the reverse proxy request doesn’t touch the public Internet. Instead, it stays within Fly.io’s private network which is great for performance and security.

The next thing I want to look into is migrating the image resizing app over to Fly.io Machines for on-demand image resizing rather than leaving servers running 24/7. However, to do that, I think imgproxy needs to implement an application shutdown option. I’ve made a GitHub issue for that feature request here. If you have workaround in mind, please let me know!

All things considered, I’m very happy with this setup. My site is still super fast around the world, and I don’t have to worry about spending too much money on Cloudflare’s image resizing service.

So, the moral of the story here is that purpose-built static hosting services offered by the likes of Vercel, Netlify, and Cloudflare are great if you don’t have custom requirements. For sites that require additional features like image resizing or reverse proxy setups, migrating to Fly.io and gaining access to a fully customizable server environment (you can choose whichever web server you want) is a huge win!