Introduction
Setting up a monorepo architecture with proper tooling can significantly improve your development workflow. Bun, the all-in-one JavaScript runtime and toolkit, offers excellent workspace support that simplifies managing multiple interconnected packages. This guide walks you through creating a Bun workspace and containerizing it with Docker — perfect for modern full-stack applications.
What You'll Learn
- How to structure a Bun workspace with shared packages
- Configuring TypeScript for cross-package imports
- Containerizing your application with Docker
- Setting up a complete development environment
Project Structure
Let's start with the folder structure we'll be creating:
├── apps/
│ ├── client/ # Frontend application
│ └── server/ # Backend application
├── infra/
│ ├── docker/ # Docker images
│ │ ├── client.Dockerfile
│ │ └── server.Dockerfile
├── packages/
│ └── shared/ # Shared code between apps
├── docker-compose.yml
└── package.json
This structure follows the monorepo pattern where:
-
apps
contains your applications (client and server) -
packages
contains shared libraries used across applications -
infra
holds infrastructure-related files like Dockerfiles
Step 1: Initialize the Main Project
First, let's initialize our root project:
bun init -y
Then modify your package.json
to configure workspaces:
{
"name": "workspace",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"type": "module",
"devDependencies": {
"@types/bun": "latest"
}
}
The workspaces
field tells Bun to treat subdirectories in packages
and apps
as separate packages within our monorepo.
Step 2: Set Up the Shared Package
Create a shared package that will contain code used by both frontend and backend:
mkdir -p packages/shared
cd packages/shared
bun init -y
Update the packages/shared/package.json
:
{
"name": "@packages/shared",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist --target node"
}
}
Create a basic source file:
mkdir -p packages/shared/src
echo 'export const greet = (name: string) => `Hello, ${name}!`;' > packages/shared/src/index.ts
Step 3: Set Up the Applications
Backend Application
Set up the server application:
mkdir -p apps/server/src
cd apps/server
bun init -y
Update apps/server/package.json
to include the shared package:
{
"name": "@apps/server",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@packages/shared": "workspace:*"
},
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts"
}
}
Create a tsconfig.json
in the server directory:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"paths": {
"@packages/shared": [
"../../packages/shared/src"
]
}
}
}
Create a simple server:
// apps/server/src/index.ts
import { greet } from '@packages/shared';
import { serve } from 'bun';
const server = serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/health') {
return new Response('OK');
}
if (url.pathname === '/') {
return new Response(greet('from the server'));
}
return new Response('Not Found', { status: 404 });
},
});
console.log(`Server running at http://localhost:${server.port}`);
Frontend Application
Set up the client application similarly:
mkdir -p apps/client/src
cd apps/client
bun init -y
Update apps/client/package.json
:
{
"name": "@apps/client",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@packages/shared": "workspace:*"
},
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts"
}
}
Create a tsconfig.json
in the client directory:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"paths": {
"@packages/shared": [
"../../packages/shared/src"
]
}
}
}
Step 4: Dockerize Your Applications
Server Dockerfile
Create infra/docker/server.Dockerfile
:
FROM oven/bun:1-slim
WORKDIR /app
# Copy only package files first
COPY package.json bun.lockb ./
COPY packages/shared/package.json ./packages/shared/
COPY apps/server/package.json ./apps/server/
# Install dependencies
RUN bun install --filter '@apps/server'
# Copy source files
COPY apps/server ./apps/server
COPY packages/shared ./packages/shared
# Build shared package
RUN cd packages/shared && bun run build
EXPOSE 3000
ENTRYPOINT ["bun", "run", "apps/server/src/index.ts"]
Step 5: Create Docker Compose File
Create docker-compose.yml
in the root directory:
version: '3.8'
services:
...
server:
build:
context: .
dockerfile: infra/docker/server.Dockerfile
ports:
- "3000:3000"
env_file:
- apps/server/.env
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:3000/health",
]
interval: 30s
timeout: 10s
retries: 3
Step 6: Install Dependencies and Run
From the root directory, install all dependencies:
bun install
You can now start everything with Docker Compose:
docker-compose up -d
Or run individual components in development mode:
# Run the server
cd apps/server
bun run dev
# Run the client
cd apps/client
bun run dev
Benefits of This Setup
Code Sharing: The shared package can contain utilities, types, constants, and business logic used by both frontend and backend.
Type Safety: TypeScript configuration ensures type safety across packages.
Development Experience: Changes in shared packages are immediately available to dependent applications.
Production Ready: Docker images are optimized for production deployment.
Scalability: This structure makes it easy to add more applications or packages as your project grows.
Conclusion
You now have a fully functional Bun workspace setup with Docker integration. This architecture promotes code sharing, provides excellent developer experience, and simplifies deployment.
For a complete working example with React frontend and a Bun backend with Prisma, visit the bun-workspaces-docker GitHub repository.
Happy coding with Bun!