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

  1. Code Sharing: The shared package can contain utilities, types, constants, and business logic used by both frontend and backend.

  2. Type Safety: TypeScript configuration ensures type safety across packages.

  3. Development Experience: Changes in shared packages are immediately available to dependent applications.

  4. Production Ready: Docker images are optimized for production deployment.

  5. 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!