| 1 | # VPS / Docker |
| 2 | |
| 3 | import { Button } from "vocs/components"; |
| 4 | |
| 5 | Serve both your frontend and API from the same process, same port, and same originβideal for fullstack apps where simplicity matters. |
| 6 | |
| 7 | This guide walks through configuring your bhvr project for single origin deployment on a VPS , where your React frontend and Hono API run from the same Bun process. |
| 8 | |
| 9 | Perfect for: |
| 10 | |
| 11 | - VPS deployments |
| 12 | - Raspberry Pis |
| 13 | - Home servers |
| 14 | - Docker containers |
| 15 | - Projects where you want one URL to rule them all |
| 16 | |
| 17 | ## Prerequisites |
| 18 | |
| 19 | This guide assumes you have a bhvr project set up. If not, start here: |
| 20 | |
| 21 | ```bash |
| 22 | bun create bhvr@latest my-app |
| 23 | cd my-app |
| 24 | ``` |
| 25 | |
| 26 | <Button href='/getting-started'>Getting Started with bhvr</Button> |
| 27 | |
| 28 | ## What Is Single Origin? |
| 29 | |
| 30 | Instead of running your client and server separately (the default bhvr setup), single origin serves everything from one process: |
| 31 | |
| 32 | **Default bhvr setup:** |
| 33 | |
| 34 | - Client runs on port 5173 (Vite dev server) |
| 35 | - Server runs on port 3000 (Hono API) |
| 36 | - Requires CORS for communication |
| 37 | |
| 38 | **Single origin setup:** |
| 39 | |
| 40 | - Everything runs on port 3000 |
| 41 | - Hono serves both API routes and static React files |
| 42 | - No CORS needed |
| 43 | |
| 44 | ## Configuration |
| 45 | |
| 46 | ### 1. Update Your Hono Server |
| 47 | |
| 48 | Modify `server/src/index.ts` to serve static files alongside your API: |
| 49 | |
| 50 | ```typescript |
| 51 | import { Hono } from "hono"; |
| 52 | import { cors } from "hono/cors"; |
| 53 | import { serveStatic } from "hono/bun"; // [!code ++] |
| 54 | import type { ApiResponse } from "shared/dist"; |
| 55 | |
| 56 | const app = new Hono(); |
| 57 | |
| 58 | // CORS is optional for single origin deployment |
| 59 | // Keep for development flexibility, remove for production if desired |
| 60 | app.use(cors()); |
| 61 | |
| 62 | // Your existing API routes - keep the /api prefix for clarity |
| 63 | app.get("/api", (c) => { |
| 64 | return c.text("Hello Hono!"); |
| 65 | }); |
| 66 | |
| 67 | app.get("/api/hello", async (c) => { |
| 68 | const data: ApiResponse = { |
| 69 | message: "Hello BHVR!", |
| 70 | success: true, |
| 71 | }; |
| 72 | return c.json(data, { status: 200 }); |
| 73 | }); |
| 74 | |
| 75 | // Add more API routes here with /api prefix |
| 76 | // app.get('/api/users', ...) |
| 77 | // app.post('/api/data', ...) |
| 78 | |
| 79 | // Serve static files for everything else |
| 80 | app.use("*", serveStatic({ root: "./static" })); // [!code ++] |
| 81 | |
| 82 | app.get("*", async (c, next) => { // [!code ++] |
| 83 | return serveStatic({ root: "./static", path: "index.html" })(c, next); // [!code ++] |
| 84 | }); // [!code ++] |
| 85 | |
| 86 | const port = parseInt(process.env.PORT || "3000"); |
| 87 | |
| 88 | export default { |
| 89 | port, |
| 90 | fetch: app.fetch, |
| 91 | }; |
| 92 | |
| 93 | console.log(`𦫠bhvr server running on port ${port}`); |
| 94 | ``` |
| 95 | |
| 96 | ### 2. Update Your React Client |
| 97 | |
| 98 | Modify `client/src/App.tsx` to use relative API paths: |
| 99 | |
| 100 | ```typescript |
| 101 | import { useState } from "react"; |
| 102 | import beaver from "./assets/beaver.svg"; |
| 103 | import { ApiResponse } from "shared"; |
| 104 | import "./App.css"; |
| 105 | |
| 106 | function App() { |
| 107 | const [data, setData] = useState<ApiResponse | undefined>(); |
| 108 | |
| 109 | async function sendRequest() { |
| 110 | try { |
| 111 | // Use relative path - works in both dev and production |
| 112 | const req = await fetch("/api/hello"); // [!code hl] |
| 113 | const res: ApiResponse = await req.json(); |
| 114 | setData(res); |
| 115 | } catch (error) { |
| 116 | console.log(error); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | return ( |
| 121 | <> |
| 122 | <div> |
| 123 | <a href='https://github.com/stevedylandev/bhvr' target='_blank'> |
| 124 | <img src={beaver} className='logo' alt='beaver logo' /> |
| 125 | </a> |
| 126 | </div> |
| 127 | <h1>bhvr</h1> |
| 128 | <h2>Bun + Hono + Vite + React</h2> |
| 129 | <p>A typesafe fullstack monorepo</p> |
| 130 | <div className='card'> |
| 131 | <button onClick={sendRequest}>Call API</button> |
| 132 | {data && ( |
| 133 | <pre className='response'> |
| 134 | <code> |
| 135 | Message: {data.message} <br /> |
| 136 | Success: {data.success.toString()} |
| 137 | </code> |
| 138 | </pre> |
| 139 | )} |
| 140 | </div> |
| 141 | <p className='read-the-docs'>Click the beaver to learn more</p> |
| 142 | </> |
| 143 | ); |
| 144 | } |
| 145 | |
| 146 | export default App; |
| 147 | ``` |
| 148 | |
| 149 | ### 3. Configure Vite for Development |
| 150 | |
| 151 | Update `client/vite.config.ts` to proxy API calls during development: |
| 152 | |
| 153 | ```typescript |
| 154 | import { defineConfig } from "vite"; |
| 155 | import react from "@vitejs/plugin-react"; |
| 156 | import path from "path"; |
| 157 | |
| 158 | export default defineConfig({ |
| 159 | plugins: [react()], |
| 160 | resolve: { |
| 161 | alias: { |
| 162 | "@client": path.resolve(__dirname, "./src"), |
| 163 | "@server": path.resolve(__dirname, "../server/src"), |
| 164 | "@shared": path.resolve(__dirname, "../shared/src"), |
| 165 | }, |
| 166 | }, |
| 167 | server: { // [!code ++] |
| 168 | proxy: { // [!code ++] |
| 169 | "/api": { // [!code ++] |
| 170 | target: "http://localhost:3000", // [!code ++] |
| 171 | changeOrigin: true, // [!code ++] |
| 172 | }, // [!code ++] |
| 173 | }, // [!code ++] |
| 174 | }, // [!code ++] |
| 175 | }); |
| 176 | ``` |
| 177 | |
| 178 | ### 4. Add Single Origin Scripts |
| 179 | |
| 180 | Add these scripts to your root `package.json` (alongside the existing bhvr scripts): |
| 181 | |
| 182 | ```json |
| 183 | { |
| 184 | "scripts": { |
| 185 | "dev": "turbo dev", |
| 186 | "dev:client": "turbo dev --filter=client", |
| 187 | "dev:server": "turbo dev --filter=server", |
| 188 | "build": "turbo build", |
| 189 | "build:client": "turbo build --filter=client", |
| 190 | "build:server": "turbo build --filter=server", |
| 191 | "build:single": "bun run build && bun run copy:static && bun run build:server", // [!code ++] |
| 192 | "copy:static": "rm -rf server/static && cp -r client/dist server/static", // [!code ++] |
| 193 | "start:single": "cd server && bun run dist/index.js", // [!code ++] |
| 194 | "lint": "turbo lint", |
| 195 | "type-check": "turbo type-check", |
| 196 | "test": "turbo test", |
| 197 | "postinstall": "turbo build --filter=shared --filter=server" |
| 198 | } |
| 199 | } |
| 200 | ``` |
| 201 | |
| 202 | ## Development vs Production |
| 203 | |
| 204 | ### Development (Default bhvr) |
| 205 | |
| 206 | Use the standard bhvr development workflow: |
| 207 | |
| 208 | ```bash |
| 209 | bun run dev |
| 210 | ``` |
| 211 | |
| 212 | This runs: |
| 213 | |
| 214 | - Client on `http://localhost:5173` (Vite dev server) |
| 215 | - Server on `http://localhost:3000` (Hono API) |
| 216 | - Vite proxy forwards `/api` calls to the server |
| 217 | |
| 218 | ### Production (Single Origin) |
| 219 | |
| 220 | Build and run from single origin: |
| 221 | |
| 222 | ```bash |
| 223 | # Build everything and prepare for single origin |
| 224 | bun run build:single |
| 225 | |
| 226 | # Start the single origin server |
| 227 | bun run start:single |
| 228 | ``` |
| 229 | |
| 230 | Your app now runs entirely on `http://localhost:3000`. |
| 231 | |
| 232 | ## Deployment |
| 233 | |
| 234 | ### Docker |
| 235 | |
| 236 | ```dockerfile |
| 237 | FROM oven/bun:latest |
| 238 | WORKDIR /app |
| 239 | |
| 240 | # Copy package files |
| 241 | COPY package.json bun.lock ./ |
| 242 | COPY client/package.json ./client/ |
| 243 | COPY server/package.json ./server/ |
| 244 | COPY shared/package.json ./shared/ |
| 245 | |
| 246 | # Copy source code |
| 247 | COPY . . |
| 248 | |
| 249 | # Install dependencies |
| 250 | RUN bun install |
| 251 | |
| 252 | # Build for single origin |
| 253 | RUN bun run build:single |
| 254 | |
| 255 | EXPOSE 3000 |
| 256 | CMD ["bun", "run", "start:single"] |
| 257 | ``` |
| 258 | |
| 259 | ### VPS / Bare Metal |
| 260 | |
| 261 | ```bash |
| 262 | # Clone your bhvr project |
| 263 | git clone <your-repo> my-app && cd my-app |
| 264 | |
| 265 | # Install and build |
| 266 | bun install |
| 267 | bun run build:single |
| 268 | |
| 269 | # Run (consider using PM2 or systemd for production) |
| 270 | bun run start:single |
| 271 | ``` |
| 272 | |
| 273 | ### Environment Variables |
| 274 | |
| 275 | Configure the port and other settings: |
| 276 | |
| 277 | ```bash |
| 278 | PORT=8080 bun run start:single |
| 279 | ``` |
| 280 | |
| 281 | ## File Structure |
| 282 | |
| 283 | After building for single origin, your bhvr project structure looks like: |
| 284 | |
| 285 | ``` |
| 286 | . |
| 287 | βββ client/ |
| 288 | β βββ dist/ # Built React app |
| 289 | β βββ src/ |
| 290 | βββ server/ |
| 291 | β βββ dist/ |
| 292 | β β βββ index.js # Built Hono server |
| 293 | β βββ static/ # Copied from client/dist |
| 294 | β βββ src/ |
| 295 | βββ shared/ |
| 296 | β βββ dist/ # Built shared types |
| 297 | β βββ src/ |
| 298 | βββ package.json |
| 299 | ``` |
| 300 | |
| 301 | ## CORS Configuration |
| 302 | |
| 303 | Since single origin serves everything from the same origin, **CORS is not required** for production. However, you might want to keep it for development flexibility: |
| 304 | |
| 305 | **Production (Single Origin):** |
| 306 | |
| 307 | - React app and API served from same origin (e.g., `https://yourapp.com`) |
| 308 | - All requests are same-origin |
| 309 | - CORS not needed |
| 310 | |
| 311 | **Development:** |
| 312 | |
| 313 | - Vite proxy handles cross-origin requests automatically |
| 314 | - CORS still optional due to proxy, but useful for: |
| 315 | - Testing API directly in browser/tools |
| 316 | - Alternative development setups |
| 317 | - Third-party integrations during development |
| 318 | |
| 319 | **To remove CORS for production**, you can conditionally apply it: |
| 320 | |
| 321 | ```typescript |
| 322 | // Only use CORS in development |
| 323 | if (process.env.NODE_ENV !== "production") { |
| 324 | app.use(cors()); |
| 325 | } |
| 326 | ``` |
| 327 | |
| 328 | Or remove the `app.use(cors())` line entirely if you don't need development flexibility. |
| 329 | |
| 330 | ## Key Benefits |
| 331 | |
| 332 | - **Simplified deployment**: One process, one port, one URL |
| 333 | - **No CORS complexity**: Frontend and API share the same origin |
| 334 | - **Maintains bhvr workflow**: Still use `bun run dev` for development |
| 335 | - **Type safety preserved**: All bhvr type sharing continues to work |
| 336 | - **Resource efficient**: Perfect for small VPS, Raspberry Pi, or containers |
| 337 | |
| 338 | ## Troubleshooting |
| 339 | |
| 340 | **API calls fail in development?** |
| 341 | |
| 342 | - Ensure Vite proxy is configured in `client/vite.config.ts` |
| 343 | - Check that your server is running on port 3000 |
| 344 | |
| 345 | **404 errors on page refresh?** |
| 346 | |
| 347 | - The `serveStatic` catchall should handle SPA routing automatically |
| 348 | - Verify client files are copied to `server/static/` |
| 349 | |
| 350 | **Build fails?** |
| 351 | |
| 352 | - Run `bun run build` first to ensure shared types are available |
| 353 | - Check that all bhvr workspaces install correctly |
| 354 | |
| 355 | ## Summary |
| 356 | |
| 357 | Single origin deployment transforms your bhvr project from a multi-port development setup into a production-ready single process application, while preserving all the type safety and development experience that makes bhvr great. |
| 358 | |
| 359 | ## More Resources |
| 360 | |
| 361 | <Button href='/getting-started'>Getting Started with bhvr</Button> |