Using LiteFS with Bun on Fly.io
Tags: bunjs, javascript, ts, sqlite
Published: 2023-02-24 06:57:30

---
title: Using LiteFS with Bun on Fly.io
published: true
description: Deploy to fly.io a bun alpine Docker image that uses SQLite via 1GB free mounted LiteFS
tags: bun, JS, TS, SQLite
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jbap8ba6quo2n6iwj58g.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-02-24 06:26 +0000
---

**Update**
There is [a repository to fork and try](https://github.com/WebReflection/fly-bun-sqlite#readme) that simplifies this whole post, specially after latest development with Fly and Bun.

- - -

As neither [Bun](https://bun.sh/) nor [LiteFS](https://fly.io/docs/litefs/) are recommended for production yet, I’ve decided it was obviously a good idea to deploy “*their synergy*” on [fly.io](https://fly.io/) 😇

“*… but why?*” … well, this is why!

### What’s Bun very good at:

![serving performance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wuezr11lk1sllqqxdcig.png)

  * 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vu622l2vhpjcpmwbswod.png)

  *     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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xnd5mqhkpq8ofry7ipqj.png)

  * 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](https://github.com/oven-sh/bun/blob/bun-v0.5.4/dockerhub/Dockerfile) (it’s ~100MB in total 😍) plus a few commands to bootstrap the server
  * the `fly.toml` contains a mix of what scaffolding [Nodejs](https://fly.io/docs/languages-and-frameworks/node/) and [LiteFS](https://fly.io/docs/litefs/example/#set-up-your-fly-io-config) 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](https://fly.io/docs/litefs/example/#creating-a-volume), using your closest free allowed location.

P.S. use the [LiteFS example](https://fly.io/docs/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

```sh
### 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

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

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

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

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

![the deployed app example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kqn80i9i2x76lqgvs4io.png)

#### Some metrics

![36 MB out of 232 MB RAM used](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uzx0qchkzvfggfgc6amw.png)

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](https://fly.io/docs/reference/volumes/).

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