Using LiteFS with Bun on Fly.io

webreflection

Andrea Giammarchi

Posted on February 24, 2023

Using LiteFS with Bun on Fly.io

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:

serving performance

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

sqlite performance

  • 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

Image description

  • 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 via bun run start (or bun start or even npm 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 a start 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
Enter fullscreen mode Exit fullscreen mode

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" ]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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')
`;
Enter fullscreen mode Exit fullscreen mode

package.json

{
  "name": "flying-bun",
  "description": "A Bun & LiteFS love 💘 affair",
  "type": "module",
  "scripts": {
    "start": "bun serve.js"
  },
  "dependencies": {
    "better-tags": "^0.1.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

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}!`);
Enter fullscreen mode Exit fullscreen mode

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.

the deployed app example

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

💖 💪 🙅 🚩
webreflection
Andrea Giammarchi

Posted on February 24, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Using LiteFS with Bun on Fly.io
bunjs Using LiteFS with Bun on Fly.io

February 24, 2023