Docker와 Bun을 사용하여 TanStack Start 배포하는 방법
Source: Dev.to

TanStack Start는 TanStack Router의 강력함에 풀스택 기능을 결합한 개발자들의 필수 프레임워크로 빠르게 자리 잡고 있습니다. 최신 기술이며, 속도가 빠르고 개발자 경험(DX)이 뛰어납니다.
하지만 현실은 그렇습니다: 노트북에서 npm run dev 로 개발하던 것을 견고하고 컨테이너화된 프로덕션 환경으로 옮기는 과정에서 대부분의 골칫거리가 시작됩니다.
이 가이드에서는 단순히 “동작하게 만들기”만 하는 것이 아니라, Docker와 Bun을 활용한 고성능 배포 파이프라인을 구축합니다. RAM에 파일을 캐시하고 압축을 처리하며, 애플리케이션을 번개 같은 속도로 제공하는 맞춤형 에셋 서버를 구현할 것입니다.
0️⃣ 전제 조건 (절대 건너뛰지 마세요!)
Dockerfile 하나도 건드리기 전에 반드시 충족해야 할 중요한 요구사항이 있습니다.
공식 TanStack Start 문서에 따르면, 현재 Bun으로 배포하려면 React 19가 필요합니다. 프로젝트가 아직 React 18이라면 서버가 충돌하거나 예측할 수 없는 동작을 할 수 있습니다.
먼저 의존성을 업그레이드하세요:
bun install react@19 react-dom@19
준비됐나요? 이제 빌드해봅시다.
1️⃣ 기본: Vite 설정
먼저, Vite가 프로덕션 환경에 준비되었는지 확인해야 합니다. @/components와 같은 경로 별칭이 올바르게 작동하도록 하고, 콘솔 로그를 제거하여 프로덕션 로그를 깔끔하게 유지하고 싶습니다.
vite.config.ts
// vite.config.ts
import { defineConfig } from "vite";
import { devtools } from "@tanstack/devtools-vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import viteTsConfigPaths from "vite-tsconfig-paths";
import tailwindcss from "@tailwindcss/vite";
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
esbuild: {
// Clean up logs in production
drop: ["console", "debugger"],
},
server: {
port: 3058,
},
plugins: [
devtools(),
// This plugin is vital for resolving paths like "@/lib/utils"
viteTsConfigPaths({
projects: ["./tsconfig.json"],
}),
tailwindcss(),
tanstackStart(),
viteReact(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
Source: …
2️⃣ 비밀 소스: 커스텀 Bun 서버
이게 바로 멋진 부분입니다. node server.js를 실행하는 대신, Bun의 네이티브 속도를 활용하는 커스텀 스크립트를 사용할 것입니다. 이 스크립트는 스마트 자산 서버 역할을 합니다: 작은 파일(아이콘, CSS, 작은 JS 청크)을 메모리(RAM)로 직접 로드하고, 자동 Gzip 압축으로 제공하며, 완벽한 브라우저 캐싱을 위한 ETag를 생성합니다.
왜?
- 속도 – RAM이 디스크보다 빠릅니다.
- 압축 – Gzip이 자동으로 처리됩니다.
- 효율성 – ETag를 통해 브라우저가 자산을 완벽히 캐시합니다.
프로젝트 루트에 server.ts 파일을 생성하세요(전체 구현은 TanStack Router 레포의 참고 구현을 확인). 아래 코드를 붙여넣습니다:
// server.ts
import path from "node:path";
// --- Configuration ---
const SERVER_PORT = Number(process.env.PORT ?? 3000);
const CLIENT_DIRECTORY = "./dist/client";
const SERVER_ENTRY_POINT = "./dist/server/server.js";
// Simple Logging Utility
const log = {
info: (msg: string) => console.log(`[INFO] ${msg}`),
error: (msg: string) => console.error(`[ERROR] ${msg}`),
success: (msg: string) => console.log(`[SUCCESS] ${msg}`),
};
// --- Asset Preloading Logic ---
// 5 MiB limit for in‑memory files
const MAX_PRELOAD_BYTES = Number(
process.env.ASSET_PRELOAD_MAX_SIZE ?? 5 * 1024 * 1024
);
// (Helper functions for ETag, Gzip, and Glob patterns omitted for brevity
// but ensure you include the full logic from the source provided earlier!)
/**
* The Main Server Initializer
*/
async function initializeServer() {
log.info("Starting Production Server...");
// 1. Load the TanStack Start handler
let handler: { fetch: (request: Request) => Response | Promise };
try {
const serverModule = (await import(SERVER_ENTRY_POINT)) as any;
handler = serverModule.default;
log.success("TanStack Start handler initialized");
} catch (error) {
log.error(`Failed to load handler: ${String(error)}`);
process.exit(1);
}
// 2. Initialise static routes (scans ./dist/client)
// Note: In your full file, ensure you include the `initializeStaticRoutes` function!
const { routes } = await initializeStaticRoutes(CLIENT_DIRECTORY);
// 3. Start Bun server
const server = Bun.serve({
port: SERVER_PORT,
routes: {
// Serve cached assets first
...routes,
// Fallback to the App handler for everything else
"/*": (req: Request) => {
return handler.fetch(req);
},
},
});
log.success(`Server listening on http://localhost:${String(server.port)}`);
}
initializeServer().catch((err) => {
log.error(String(err));
});
// ... (Paste the rest of the helper functions: initializeStaticRoutes, etc. here)
중요: 전체 캐싱 혜택을 얻으려면 공식 자료에 있는 전체
server.ts코드를 사용하세요.
3️⃣ Dockerfile: 작고 효율적
우리는 최종 이미지를 작게 유지하기 위해 멀티‑스테이지 빌드를 사용할 것입니다. (Dockerfile의 나머지 내용도 동일한 패턴을 따릅니다 – 빌드된 자산을 복사하고, 프로덕션 의존성만 설치하며, Bun 서버를 실행합니다.)
# ---- Builder Stage ----
FROM oven/bun:1 AS builder
WORKDIR /app
# Install dependencies (including React 19)
COPY package.json bun.lockb ./
RUN bun install --production=false
# Copy source files
COPY . .
# Build the client and server bundles
RUN bun run build
# ---- Runtime Stage ----
FROM oven/bun:1-alpine AS runtime
WORKDIR /app
# Copy only the built output and production deps
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
COPY --from=builder /app/bun.lockb .
# Install only production dependencies
RUN bun install --production
# Expose the port the server listens on
EXPOSE 3000
# Start the Bun server
CMD ["bun", "run", "server.ts"]
이 Docker 설정이 작동하는 이유
우리는 최종 Docker 이미지를 작게 유지합니다. 빌드 도구(TypeScript, Vite 등)를 프로덕션 컨테이너에 포함시키지 않고, 앱을 실행하는 데 필요한 것만 포함하기 때문입니다.
핵심 기술 세부 사항
Vite는 빌드 시점에 환경 변수가 필요합니다(예: API URL) — 이를 통해 클라이언트‑사이드 JavaScript에 “베이킹”할 수 있습니다. Dockerfile의 ARG 섹션에 특히 주의하세요.
Dockerfile (멀티‑스테이지)
# syntax=docker/dockerfile:1
# =============================================
# Stage 1: Builder
# =============================================
FROM oven/bun:1-alpine AS builder
WORKDIR /app
# Install dependencies (cached layer)
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Copy the source code
COPY . .
# --- IMPORTANT ---
# Vite needs these variables AT BUILD TIME to replace import.meta.env values
ARG VITE_API_URL
ARG VITE_BETTER_AUTH_URL
ENV VITE_API_URL=${VITE_API_URL}
ENV VITE_BETTER_AUTH_URL=${VITE_BETTER_AUTH_URL}
# Build the app
RUN bun run build
# =============================================
# Stage 2: Runner (Production)
# =============================================
FROM oven/bun:1-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Install curl for healthchecks
RUN apk add --no-cache curl
# Copy package files
COPY package.json bun.lock ./
# Install ONLY production deps (react, etc.)
RUN bun install --production
# Copy the build artifacts from the Builder stage
COPY --from=builder --chown=bun:bun /app/dist ./dist
COPY --from=builder --chown=bun:bun /app/server.ts ./server.ts
USER bun
# The port is dynamic via Env Vars
ENV PORT=3058
EXPOSE 3058
# Start the custom Bun server
CMD ["bun", "server.ts"]
4️⃣ Orchestration: Docker Compose
Docker Compose를 사용하면 환경 변수, 포트 및 헬스 체크를 쉽게 관리할 수 있습니다.
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
# Pass these to the Builder stage!
VITE_BETTER_AUTH_URL: ${VITE_BETTER_AUTH_URL}
VITE_API_URL: ${VITE_API_URL} # (optional, if needed at build time)
environment:
# Runtime config for the server
NODE_ENV: production
PORT: ${PORT:-3058}
# App secrets & configs
VITE_API_URL: ${VITE_API_URL}
DATABASE_URL: ${DATABASE_URL}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
# SMTP / Email config
HOST_EMAIL: ${HOST_EMAIL}
HOST_PORT: ${HOST_PORT}
HOST_AUTH_USER: ${HOST_AUTH_USER}
HOST_AUTH_PASS: ${HOST_AUTH_PASS}
ports:
# Map host port to container port
- '${PORT:-3058}:${PORT:-3058}'
restart: always
healthcheck:
# Ping the server to ensure it's alive
test: ['CMD-SHELL', 'curl -fsS http://localhost:${PORT:-3058} || exit 1']
interval: 10s
timeout: 5s
retries: 5
결론
이제 컨테이너 안에서 개발 서버를 실행하지 않습니다. 이 설정은 고도로 최적화된, 메모리‑캐싱이 적용된, Bun‑기반 프로덕션 환경을 제공합니다.
빠른 요약
- React 19 – Bun 개발에 필수
- server.ts – 고급 캐싱 및 gzip 압축 제공
- Dockerfile – 빌드 시 환경 변수를 올바르게 처리하는 다단계 빌드
- Docker Compose – 헬스 체크와 함께 런타임을 오케스트레이션
- 필요한 변수들을 포함한
.env파일을 생성합니다. - 실행:
docker compose up --build -d
- TanStack Start 앱이 날아다니는 모습을 확인하세요!
자세한 내용은 제 블로그를 방문해 주세요.
