Better Programming

Advice for programmers.

Follow publication

Using LiteFS With Bun on Fly.io

Learn how to deploy your next Bun project on the cloud — with a free database

Andrea Giammarchi
Better Programming
Published in
7 min readFeb 22, 2023

A picture of delicious food by me, 30 minutes ago (at the time of writing)

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 fast at bootstrapping, making it ideal for lambda’s cold and hot starts, together with docker-based containers
  • It’s 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 good at

  • It provides a whole GB of the mounted filesystem that could host one-to-many SQLite databases for free (1 GB 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 a hosted solution is nowhere near as simple and well done as it is for the fly.io setup, which we’ll check in detail now!

The Project Tree

folders and files of the project
  • 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 set up via Bun run start (or npm start if the 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 a unique module entry point, plus some template literal-based utilities
  • The package.json is used to provide a start command and optional dependencies
  • The serve.js file starts a server demo that shows 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 starter (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's 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 goes fine, you should be able to reach https://YOUR_FLY_APP_NAME.fly.dev and see at least one record shown on the page.

Some metrics

36 MB out of 232 MB RAM used

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 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

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 (one for IP geo-location, one for blog text, one for users to admin, etc.)

The most underestimated part of SQLite as a database, besides 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, as I’ve just done in here.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Andrea Giammarchi
Andrea Giammarchi

Written by Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

No responses yet

Write a response