diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a9d6755 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.env* +.git +e2e +dist +*.test.ts +*.test.tsx +*.spec.ts +*.spec.tsx +.vscode +.idea +.claude diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..58ff5ec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Stage 1: Install dependencies +FROM oven/bun:1 AS install +WORKDIR /app + +COPY package.json bun.lock ./ +COPY client/package.json ./client/ +COPY server/package.json ./server/ +COPY core/package.json ./core/ + +RUN bun install --frozen-lockfile + +# Stage 2: Build +FROM oven/bun:1 AS build +WORKDIR /app + +COPY --from=install /app/node_modules ./node_modules + +COPY . . + +RUN cd server && bunx prisma generate +RUN cd client && bunx vite build + +# Stage 3: Production +FROM oven/bun:1 AS production +WORKDIR /app + +COPY --from=install /app/node_modules ./node_modules +COPY --from=build /app/client/dist ./client/dist + +COPY server ./server +COPY core ./core +COPY package.json ./ + +# Overlay generated Prisma client from build stage +COPY --from=build /app/server/src/generated ./server/src/generated + +ENV NODE_ENV=production +EXPOSE 3000 + +CMD ["sh", "-c", "cd server && bunx prisma migrate deploy && cd .. && bun run server/src/index.ts"] diff --git a/railway.toml b/railway.toml new file mode 100644 index 0000000..ec77387 --- /dev/null +++ b/railway.toml @@ -0,0 +1,8 @@ +[build] +dockerfilePath = "Dockerfile" + +[deploy] +healthcheckPath = "/api/health" +healthcheckTimeout = 30 +restartPolicyType = "ON_FAILURE" +restartPolicyMaxRetries = 5 diff --git a/server/src/index.ts b/server/src/index.ts index 3bcbbca..3fa6ef5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,3 +1,4 @@ +import path from "path"; import Sentry from "./lib/sentry"; import express from "express"; import cors from "cors"; @@ -66,6 +67,17 @@ app.use("/api/webhooks", webhooksRouter); Sentry.setupExpressErrorHandler(app); +// In production, serve the built React client as static files +if (isProduction) { + const clientDist = path.resolve(import.meta.dirname, "../../client/dist"); + app.use(express.static(clientDist)); + + // SPA fallback: serve index.html for any non-API route + app.get("/{*path}", (_req, res) => { + res.sendFile(path.join(clientDist, "index.html")); + }); +} + if (!process.env.WEBHOOK_SECRET) { console.warn("Warning: WEBHOOK_SECRET is not set. Webhook endpoints will return 500."); }