Using LiteFS with Bun on Fly.io
Andrea Giammarchi
Posted on February 24, 2023
Update
There is a repository to fork and try that simplifies this whole post, specially after latest development with Fly and Bun.
As neither Bun nor LiteFS are recommended for production yet, I’ve decided it was obviously a good idea to deploy “their synergy” on fly.io 😇
“… but why?” … well, this is why!
What’s Bun very good at:
- it’s very fast at bootstrapping, making it ideal for lambdas cold and hot starts, together with docker based containers 🌟
- it’s very fast at serving, making it an ideal runtime for anything cloud related (plus it’s TS/JSX compatible without extra tooling needed!) 🌈
- it’s the fastest JS/TS runtime out there when it comes to SQLite 🦄 … as a matter of fact, bun has SQLite directly built and bound in its core so that it easily competes with any other typed PL with SQLite bindings 🚀
What’s LiteFS on Fly.io very good at:
- it provides for free a whole GB of mounted filesystem that could host 1 to many SQLite databases (1GB is a lot of text!!!) 🤩
- it can replicate and sync databases across the globe when/if needed (pay as you go or proper plan needed but we can start for free) 🥳
- fly.io allows any Docker image, so that we can test both locally and deploy in production with ease 🤠
On top of that, the landscape around SQLite as hosted solution is nowhere nearly as simple and well done as it is for fly.io setup, which we’re going to check in details now!
The Project Tree
- the litefs folder is used instead of the real mounted litefs path whenever we’re testing locally and/or not in production. Let’s just type
mkdir litefs
in the directory we’d like to use to test this setup viabun run start
(orbun start
or evennpm start
if node is present and bun available) - the
.dockerignore
file contains all possible stuff we shouldn’t push to docker - the
Dockerfile
contains the bun’s official alpine based docker image (it’s ~100MB in total 😍) plus a few commands to bootstrap the server - the
fly.toml
contains a mix of what scaffolding Nodejs and LiteFS prepared examples would look like - the
litefs.js
file handles the database connection as unique module entry point, plus some template literal based utilities - the
package.json
is used to provide astart
command and optional dependencies - the
serve.js
file simply starts a server demo that show some welcome and all the rows in the dummy/example SQLite database
All files are going to be shown with their content too.
Before we start
The easiest way to scaffold a fly.io project is to use fly apps create
, which will generate a unique YOUR_FLY_APP_NAME
(see fly.toml later on) and it will give you indications of regions you can use to deploy your app, then you need to create your LiteFS volume, using your closest free allowed location.
P.S. use the LiteFS example if you don’t know where or how to start, as it’s been updated recently and it really works out of the box as a started (but it uses GO, which “can go” (dehihi) right after 😉
Once you’ve done that, each file in the list is mandatory and this is what I have as each file content:
.dockerignore
.git
litefs
node_modules
.dockerignore
bun.lockb
Dockerfile
fly.toml
Dockerfile
### GLOBALS ###
ARG GLIBC_RELEASE=2.34-r0
### GET ###
FROM alpine:latest as get
# prepare environment
WORKDIR /tmp
RUN apk --no-cache add unzip
# get bun
ADD https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip bun-linux-x64.zip
RUN unzip bun-linux-x64.zip
# get glibc
ARG GLIBC_RELEASE
RUN wget https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_RELEASE}/glibc-${GLIBC_RELEASE}.apk
### IMAGE ###
FROM alpine:latest
# install bun
COPY --from=get /tmp/bun-linux-x64/bun /usr/local/bin
# prepare glibc
ARG GLIBC_RELEASE
COPY --from=get /tmp/sgerrand.rsa.pub /etc/apk/keys
COPY --from=get /tmp/glibc-${GLIBC_RELEASE}.apk /tmp
# install glibc
RUN apk --no-cache --force-overwrite add /tmp/glibc-${GLIBC_RELEASE}.apk && \
# cleanup
rm /etc/apk/keys/sgerrand.rsa.pub && \
rm /tmp/glibc-${GLIBC_RELEASE}.apk && \
# smoke test
bun --version
#######################################################################
RUN mkdir /app
WORKDIR /app
# NPM will not install any package listed in "devDependencies" when NODE_ENV is set to "production",
# to install all modules: "npm install --production=false".
# Ref: https://docs.npmjs.com/cli/v9/commands/npm-install#description
ENV NODE_ENV production
COPY . .
RUN bun install
LABEL fly_launch_runtime="bun"
WORKDIR /app
ENV NODE_ENV production
CMD [ "bun", "run", "start" ]
fly.toml
app = "YOUR_FLY_APP_NAME"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[env]
PORT = "8080"
[experimental]
auto_rollback = true
enable_consul = true
[mounts]
source = "litefs"
destination = "/var/lib/litefs"
[[services]]
http_checks = []
internal_port = 8080
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
litefs.js
import {existsSync} from 'node:fs';
import {join} from 'node:path';
import {Database} from 'bun:sqlite';
import createSQLiteTags from 'better-tags';
// use mounted point on production, use local folder otherwise
const litefs = join(
process.env.NODE_ENV === 'production' ? '/var/lib' : '.',
'litefs'
);
// if litefs folder doesn't exist get out!
if (!existsSync(litefs)) {
console.error('Unable to reach', litefs);
process.exit(1);
}
// shared db + template literals based utilities
const {db, get, all, values, exec, run, entries, transaction} =
createSQLiteTags(new Database(join(litefs, 'db')));
export {db, get, all, values, exec, run, entries, transaction};
///////////////////////////////////////////////////////////////
// FOR DEMO SAKE ONLY - EXECUTED ON EACH DEPLOY
///////////////////////////////////////////////////////////////
// some table schema
exec`
CREATE TABLE IF NOT EXISTS persons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT NOT NULL,
company TEXT NOT NULL
)
`;
// some table entry
exec`
INSERT INTO persons
(name, phone, company)
VALUES
(${
crypto.randomUUID()
}, ${
('+' + (Date.now() * Math.random())).replace(/[^+\d]/, ' ')
}, 'fly.io')
`;
package.json
{
"name": "flying-bun",
"description": "A Bun & LiteFS love 💘 affair",
"type": "module",
"scripts": {
"start": "bun serve.js"
},
"dependencies": {
"better-tags": "^0.1.2"
}
}
serve.js
import {serve} from 'bun';
// grab the db or some utility
import {all} from './litefs.js';
// grab the port and start the server
const port = process.env.PORT || 3000;
serve({
fetch(request) {
const greeting = "<h1>Hello From Bun on Fly!</h1>";
const results = `<pre>${JSON.stringify(
all`SELECT * FROM persons ORDER BY id DESC`, null, '\t')
}</pre>`;
return new Response(greeting + '<br>' + results, {
headers: {'Content-Type': 'text/html; charset=utf-8'}
});
},
error(error) {
return new Response("Uh oh!!\n" + error.toString(), { status: 500 });
},
port
});
console.log(`Flying Bun app listening on port ${port}!`);
Deploy Bun on LiteFS
That’s pretty much it, once you have a unique app name, a LiteFS mounted directory you can reach, and the provided code, you can either bun run start
locally, maybe after a bun install
or an npm install
, and finally fly deploy
to see your “hello bun” running from the cloud 🌤
If everything went fine, you should be able to reach https://YOUR_FLY_APP_NAME.fly.dev
and see at least one record shown in the page.
Some metrics
The basic alpine image with just bun and glibc on it, plus the project files, should consume no more than 40MB of RAM out of the 232 MB allowed, but the cool part of fly.io is that we can always opt in for a pay as you go plan to scale CPUs, RAM, replicated databases, or increase the mounted DB size too with ease, whenever we’ll manage to reach 1 GB of mounted LiteFS file size limit. A detailed guide on how to manage fly volumes can be found here.
And That’s All Folks 🥳
Congratulations! You’ve learned how to deploy your next bun project on the cloud and with a free database solution that will host up to a GB for free and not limited to a single DB, so that separating concerns per DB is also a possibility (1 for IP geo-location, 1 for blog text, 1 for users to admin, 1 for …)
The most under-estimated part of SQLite as database, beside being the coolest embedded database that exists on earth, is that no secret user and password would ever leak, and fly.io mounted filesystems are also encrypted so that a whole class of security concerns is automatically removed from all equations and responsibilities for people sharing code, like I’ve just done in here 👋
Posted on February 24, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.