Using LiteFS With Bun on Fly.io
Learn how to deploy your next Bun project on the cloud — with a free database

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

- 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 typemkdir litefs
in the directory, we’d like to use to test this set up viaBun run start
(ornpm 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 astart
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

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.