Hosting web sites with databases is too damn expensive if you follow the instructions on Render, Digital Ocean, Heroku, etc. They all suggest you connect a \$15+/month managed database to your rinky-dink Python app, and you end up paying like $25/month and still having strict limitations. Meanwhile, many people claim SQLite is a perfectly good production database for small web sites, but nobody tells you how to actually deploy it with persistent storage.
Well, I figured it out. Here it is. Fork this repo, change the service name in render.yaml
, modify the code to your heart's content, and deploy it to render.com for $8/mo. Or you can deploy to Fly.io on the free tier, capped at $2/mo if you exceed it.
The demo deployments (the lowest tiers of Render and Fly.io) can do 330 and 110 requests per second, respectively, measured from a home internet connection in San Francisco, CA using apib
. These are honestly really horrible numbers, but probably just reflect the cheap vCPUs they are deployed on.
This setup does not do zero-downtime deployments. Your web site will go down for about a minute during each deploy. 😱‼️
Although I've done my best to test this code and these instructions, it's still just a small weekend experiment, so there might be mistakes.
It's 95% Flask boilerplate.
Features:
Common workflows are written as Make commands. These docs assume you're using macOS, but everything should translate to Linux other than some installation steps.
Python dependencies are managed using Poetry in development, and using Pip in production.
poetry init
poetry install
Migrations are always applied on the command line, never automatically.
make local-runmigrations
make serve
FLASK_SECRET_KEY
: a random string (https://www.uuidgenerator.net).FLASK_MAINTENANCE_MODE
: 1
(this will run your first deploy in maintenance mode so you can run migrations)make maintenance-runmigrations
.FLASK_MAINTENANCE_MODE
to 0
, and Render will redeploy the site. You should now be able to use the database.Familiarize yourself with Flask-Migrate. Unfortunately for all us web backend developers, we can never escape database migrations, and we need to do them right.
Quick note: it's probably not necessary to use maintenance mode to run migrations, but I haven't had time to update and test new instructions yet.
Whenever you make a change to your database, follow these steps:
make local-db-migrate
(alias for poetry run flask --app server db migrate
) to create the migration files. Check them by hand.make local-db-upgrade
(alias for poetry run flask --app server db upgrade
)FLASK_MAINTENANCE_MODE=1
).make maintenance-db-upgrade
.FLASK_MAINTENANCE_MODE=0
).deferred()
wrappers.fly.toml
, set FLASK_MAINTENANCE_MODE
to 1
(instead of 0
) so your first deploy runs in maintenance mode.fly deploy
to create and deploy an app. (You might need to use fly launch
instead, I forget. Someone please send me a PR to update this sentence.)fly secrets set FLASK_SECRET_KEY=(random string)
(https://uuidgenerator.net).fly ssh console
. In the SSH session, cd /code && make maintenance-db-upgrade
. (It should be possible to get this down to one line, but I'm having trouble with fly ssh console -C
.)fly.toml
, set FLASK_MAINTENANCE_MODE
back to 0
.fly deploy
to redeploy the site without maintenance mode.Familiarize yourself with Flask-Migrate. Unfortunately for all us web backend developers, we can never escape database migrations, and we need to do them right.
Quick note: it's probably not necessary to use maintenance mode to run migrations, but I haven't had time to update and test new instructions yet.
Whenever you make a change to your database, follow these steps:
make local-db-migrate
(alias for poetry run flask --app server db migrate
) to create the migration files. Check them by hand.make local-db-upgrade
(alias for poetry run flask --app server db upgrade
)fly secrets set FLASK_MAINTENANCE_MODE=1
).fly ssh console
. In the SSH session, cd /code && make maintenance-db-upgrade
. (It should be possible to get this down to one line, but I'm having trouble with fly ssh console -C
.)fly secrets set FLASK_MAINTENANCE_MODE=0
).deferred()
wrappers.All Python code is inside server
, leaving you space to create a client
directory for rich JS apps if you like.
All view functions are inside Flask Blueprints. Each blueprint is defined in a file with a bp_
prefix. I like this prefix because it keeps the directory flat and makes imports look really obvious, but of course you can rename the files if you want.
bp_maintenance.py
contains the routes for maintenance mode (every page will say "this web site is in maintenance mode"). You can remove this file and the call to it in create_app.py
if you can handle the SQLite database being opened in read-only mode, which is probably nicer.
inside
refers to the logged-in-user-oriented views (like "dashboard"), and outside
refers to logged-out-user-oriented views (like "index", the landing page).
Render automatically backs up the disk every day, so you have data from at most 24 hours ago.