In this post, we will scaffold a simple Express project using Bun, and then deploy it to Render using Docker. You’ll also get to see how much easier it is to get up and running locally with Bun than it is with Node.
Let’s get started!
Links
Some links for reference:
- Bun installation docs page here
- Bun + Express docs page here
- Bun + Docker docs page here
- Bun hot reload docs page here
Initial Setup
To get started, run this command to setup a Bun project in the folder of your choice:
bun init -y
Next, let’s make this a repo. Setup one on GitHub, then run the following commands:
git init
git branch -M main
git add .
git commit
git remote add origin https://github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME.git
git push -u origin main
Now add express
and its @types
package:
bun add express @types/express
Update index.ts
with this:
import express from "express";
const app = express();
const port = 8080;
app.get("/", (req, res) => {
// send a simple json response
res.json({ message: "Hello World!" });
});
app.listen(port, () => {
console.log(`Listening on port ${port}...`);
});
To test the local Express server, run this command:
bun index.ts
Then if you visit http://localhost:8080/ in the browser, or send a GET
request to that URL with something like Postman / Insomnia, you should see the JSON response:
{
"message": "Hello World!"
}
However, it would be nice to hot reload the server when we make changes, say to the response sent in app.get()
.
Thankfully, Bun has built in hot reload support. We can run this command instead:
bun --hot run index.ts
So now if we change the res.json()
to this:
app.get("/", (req, res) => {
// send a simple json response
res.json({ message: "Hello from Bun & Express!" });
});
And if you then refresh the browser page or resent your GET request, you should see the updated JSON response:
{
"message": "Hello from Bun & Express!"
}
Let’s add this as a script
to our package.json
:
"scripts": {
"dev": "bun --hot run index.ts"
}
Now we can just run bun run dev
instead. Noice!
Comparison to Node
Now, let’s take a look at a simplified breakdown of what it would take to reproduce the same result using Node:
- Create a
package.json
:npm init -y
- Create a
tsconfig.json
:npx tsc --init
- Create
index.ts
:touch index.ts
- Install packages:
npm i express @types/express concurrently nodemon
- Add the same boilerplate Express code to
index.ts
- Watching file changes:
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"”
As you can see, here there are a few extra steps, and certain things that Bun does for us automatically, such as creating the tsconfig.json
and index.ts
, we have to do ourselves. We also have to install a few extra dependencies (concurrently
and nodemon
) to get the server up and running as well.
Please don’t take this as me bashing Node, that’s not my intention at all. All I’m trying to show is how much simpler things are with Bun, and I was honestly just so shocked at how fewer steps there were, and it’s provided a better developer experience in my opinion. I definitely think Bun is worth installing and trying out!
Deploying to Render using Docker
Setup Docker
At the root level of the project, run the following command to create the necessary Docker related files:
touch Dockerfile .dockerignore
Add the following code to Dockerfile
:
# Dockerfile
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 as base
WORKDIR /usr/src/app
# install dependencies into temp folder
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy node_modules from temp folder
# then copy all (non-ignored) project files into the image
FROM install AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# [optional] tests & build
# ENV NODE_ENV=production
# RUN bun test
# RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/index.ts .
COPY --from=prerelease /usr/src/app/package.json .
# run the app
USER bun
EXPOSE 3000/tcp
CMD ["bun", "run", "dev"]
Add the following code to .dockerignore
:
# .dockerignore
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*
You can test the Dockerfile
locally if you have Docker installed by running the following command:
docker build --pull -t bun-express .
docker run -d -p 8080:8080 bun-express
If you should encounter the following error message during the build
process:
error: lockfile had changes, but lockfile is frozen
This happens because it is likely that the bun.lockb
file is not up to date. Simply run bun install
to download and reinstall all the packages, and the issue should be resolved.
Make sure to push up any changes to your repo on GitHub at this point.
Deploy to Render
Next, we can deploy the app to Render. There are of course tons of options to deploy an application in a Docker Container, I just found Render to be very beginner friendly and easy to use.
Create a free account, and once you’re logged in, on the dashboard page, click New Web Service.
Make sure Build and deploy from a Git repository is selected, and click Next.
Click the big purple Connect Repository button.
Select where you want to install Render, and then select if you want to install it for all your repos, or just a select few. I chose to install it just for this repo.
Click Install.
You should be back on the Render dashboard, and see the repo listed. Click the Connect button next to it.
Give it a name, I called mine bun-express-render
.
Select the Region best suited to you.
Ensure the Branch is set to main
.
The Runtime should automatically be set to Docker.
You can start with the Free tier.
Click Create Web Service.
The build will take a few minutes.
Once the build is finished, if you visit the URL, you should see the message
!
And that’s it! You’ve now created an Express app using TypeScript with Bun, and deployed it to Render using Docker!
Finally, here is a link to my repo with the full source code you can use as a reference in case you get stuck.
Cheers, and happy coding!
Top comments (3)
Hey, Love the post
i have an error when setting up docker
error: EACCES reading "/usr/src/app/node_modules/proxy-from-env/index.js"
Same here! Have you found out the problem?
This is a permissions issue where files are not owned by user
bun
.It can be fixed by either:
1) set all the files to be owned by user bun. Place following just above line with
USER bun
2) or by removing this line from docker file.
USER bun
This will cause bun to run as root inside the container.