Software Engineer at Capital One UK
I have a side project, an iOS app called Curry: Currency Conversion (aptly or terribly named? 🤔). A very simple currency conversion app which makes a network call to fetch the latest conversion rate. But currency conversion API services can charge per API call. To limit the number of calls, I created a server app to call those currency APIs and cache the conversion rate. So the client iOS app can call the server app without API charges. The server app is basically a proxy between the client app(s) and the currency API.
The problem came when Heroku cancelled their free tier in November 2022. From a simple side project with free hosting and no revenue to a side project that cost me ~£12 per month to keep alive, and still no revenue. I thought about shutting it down but ~£12/month seemed like a good punishment for me to go and migrate the server to somewhere else.
But things got in the way, and I kept thinking I would migrate it to the holy-grail AWS. That created more friction in getting started. Then I went to Server-Side Swift Conference in December 2022, and saw a talk by Mikaela about hosting your Server side swift app on Fly.io. She mentioned Fly.io was a great alternative to Heroku. There were also some talks about hosting a server-side swift on AWS too which I thought I would have a look at as it was my original goal.
So I had this post-it note on my Google Jam board. And even though I had thought about AWS, I checked out Fly.io first.
Fly.io have this too-good-to-be true feature—“Copy from Heroku”. The press-and-it-just-work kind of solution. (they even have an entire documentation page on how to migrate from Heroku)
The copy from Heroku to Fly.io page
It only took me a few minutes to find my Heroku app info and enter it on to the form. And it did (kind of) work! I could send a query a currency conversion rate from the newly copied Fly.io app. But it didn’t always work. Curry’s server app uses a Redis instance to cache the currency conversion rate, and that was hosted as a separate entity on Heroku so my new Fly.io app still relies on that. It was impressive though that the new app could connect to the Heroku’s Redis instance.
There was still some more work to do to migrate it properly. But I think this Fly.io feature serves as an excellent compatibility check. It validated to me my app would work on Fly.io. So I decided to migrate my server app there (and will probably host my next hobby projects here too!)
So, what I had from the copy button was the workers from Heroku.
And some environment variables
Things it didn’t copy
Things to do
If you use Heroku CLI to deploy your server app, Fly.io has its CLI tool as well.
brew install flyctl
Then I use the
launch command to create a new Fly.io app.
Note that my server app was built with the Swift Vapor framework, and it had a Dockerfile for creating a Docker image. Fly.io picked this up when I recreated the project.
If you choose to deploy now, the server app will appear on your Fly.io dashboard shortly after.
This also generates a fly.toml file, a Heroku’s Profile equivalent.
It’s helpful to have the server app on your dashboard so you can configure stuff from there. You won’t see any running processes (or dynos if it was Heroku) yet. That’s where the fly.toml file comes in.
In addition to what came with the autogenerated file, I had to add new “processes” which is an equivalent of Heroku dyno workers specified in a Procfile.
-web: Run serve --env production --hostname 0.0.0.0 --port $PORT -scheduledworker: Run queues --scheduled
[processes] web = "serve --env production --hostname 0.0.0.0 --port 8080" scheduledworker = "queues --scheduled"
There are some modifications to the script which I found out through the error messages when I try to deploy the app.
Runcommand from both. This will be duplicated. The fly.io build process prefixes the
$PORTenv variable and replace it with 8080. I still don’t know where that
PORTvariable was specified on Heroku so I just replaced with the exact port—8080.
Also, because I called the service/process “web”, I updated the
processes property under the
[[services]] section as well.
... [[services]] http_checks =  internal_port = 8080 processes = ["web"] // previously ["app"] protocol = "tcp" script_checks =  ...
And because I’m creating a brand new project, I need to copy secrets manually to the project. You can do that via the CLI as well
flyctl secrets set <key>=<value>
Or you can do it on the Fly.io dashboard if you have chosen to deploy it at launch and can see the project on the dashboard.
Also, I have just found out as I’m writing this, you can import Heroku app’s secrets to Fly.io by chaining both some CLI commands together using the Bash’s pipe operator.
heroku config -s | grep -v -e "DATABASE_URL" -e "REDIS_URL" -e "REDIS_TLS_URL" | fly secrets import
Check out their docs for more info https://fly.io/docs/rails/getting-started/migrate-from-heroku/
Note that every time you set a new secret, a new server app will be created.
Once you have configured all your processes/workers, then it’s time to deploy again.
One main add-on my app had on Heroku was a Redis instance. Fly.io supports a number of database and storage services, including Redis.
The Redis guideline here from Fly.io is pretty straightforward, so I won’t talk about it much here. You can create it via the CLI too. What I had to do was to spin it up, get the URL of the redis instance, and swap that with the Heroku one.
And once you have deployed your Redis instance, it’ll appear on the Fly.io dashboard as well.
I hard-coded the server URL into the iOS app. So when it comes to changing the database, I rely on users to download my new app version, which points to the new Fly.io server.
I could put the server URL on a text file, store that on AWS S3 or something, and make the iOS app grab the URL from that text file so I can change the URL to whatever I want whenever I want. But that’s another story for probably another blog post. This was a side project after all, so I didn’t think about that from the beginning.
In the end, I resorted to the App Store Phased Release feature, which I usually use when it comes to risky app releases. This allows me to gradually release a new version of the app, which uses the new Fly.io server and see whether there are any issues.
Fly.io server app load dashboard
Phase release progress
After the phased release finished, I let the Heroku server run for another three days, once there was no traffic, I shut it down.
Heroku traffic dashboard
Heroku resources dashboard
Funnily enough, on the last day of the phased release, 6th Jan 2023, I received my Heroku bill.
What does it look like now on Fly.io?
It’s zero on Fly.io, of course, as my app doesn’t have that many users, probably <100 active users per day.
Despite my focus on billing, it’s not to say that any free software/platform is superior to the non-free ones. As a software developer, I need to eat, so I need to get paid for the software I build. And if I happen to build a project with 100k active users, there’s probably a way I can make that profitable, and therefore consider paying for some convenient hosting services like Heroku and Fly.io paid tiers.
I think Heroku is still a great choice for hosting apps. They have a rich library of add-ons on various categories, i.e. data stores, monitoring, notifications and security. Those add-ons usually have a free tier too.
For hobbyists who want to stay lean and keep their side projects free of charge, Fly.io is a great choice. They might not have as many addons as Heroku, but they do offer the basic ones that should be sufficient to get your started.
As I’m writing this blog, I’ve just found out Heroku has an “Eco” tier. Not sure how well this works but might be a good option as well.
Edit: I’ve just seen this tweet from Danny Postma that his Heroku account was deleted and that brought down all his applications 😱. He migrated his apps to https://render.com/, which might be another option to consider.
36 hours later. Radio silence.— Danny Postma (@dannypostmaa) February 13, 2023
Full loss of data for thirteen projects.
Imagine if my databases were with them too 😧 https://t.co/pGCOR0J1u2 pic.twitter.com/ZOnqnjU5iK