Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/web-bun.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# The web app (pairux.com and installer.pairux.com on dev2) builds and runs on
# Bun. pnpm stays the monorepo's package manager and Next builds on Node (see
# apps/web/Dockerfile); this checks what the image does: install with pnpm,
# build, then boot the standalone server under Bun.
name: web on Bun

on:
push:
branches: [master]
pull_request:
paths:
- 'apps/web/**'
- 'packages/**'
- 'pnpm-lock.yaml'
- '.github/workflows/web-bun.yml'

permissions:
contents: read

jobs:
build-and-boot:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
# Placeholders: the build only needs the public values to exist.
NEXT_PUBLIC_SUPABASE_URL: https://example.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder
NEXT_PUBLIC_APP_URL: http://127.0.0.1:8080
NEXT_TELEMETRY_DISABLED: 1
steps:
- uses: actions/checkout@v6
# pnpm/action-setup reads pnpm@9.15.0 from packageManager.
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: pnpm
# The same Bun the image pins (apps/web/Dockerfile).
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.0
- run: pnpm install --frozen-lockfile --filter @pairux/web...
- run: pnpm --filter @pairux/shared-types build
- name: Build the web app
run: pnpm --filter @pairux/web build
- name: Standalone server answers under Bun
run: |
cd apps/web
cp -r public .next/standalone/apps/web/
cp -r .next/static .next/standalone/apps/web/.next/
(cd .next/standalone && PORT=8080 HOSTNAME=127.0.0.1 bun apps/web/server.js > /tmp/server.log 2>&1 &)
for i in $(seq 1 30); do
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/health || true)
[ "$code" = 200 ] && { echo "/api/health -> $code"; exit 0; }
sleep 1
done
cat /tmp/server.log; exit 1
43 changes: 30 additions & 13 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
# ===========================================
# PairUX Web App - Railway Dockerfile
# PairUX Web App (pairux.com, installer.pairux.com on dev2)
# ===========================================
# Multi-stage build for optimized production image
# Uses Node.js 24 Alpine for minimal footprint
# Runs on Bun. pnpm stays this monorepo's package manager: the desktop
# (Electron) and mobile (Expo) toolchains, the patched @expo/cli
# (pnpm.patchedDependencies) and the release scripts all depend on it, and
# none of that runs here. So Node + pnpm install dependencies and build in the
# first stages (see the build step for why Next builds on Node), and the
# standalone server runs under Bun on Alpine.
#
# Alpine 3.24 at run time, as under node:24-alpine: call analysis shells out
# to ffmpeg/ffprobe (src/lib/call-analysis/media.ts), and oven/bun:*-alpine is
# Alpine 3.22 (older ffmpeg). So the runtime is alpine:3.24 with Bun's musl
# binary copied in, the same pattern icemap.app runs in production.
# ===========================================

FROM oven/bun:1.4.0-alpine AS bun

# Stage 1: Dependencies
FROM node:24-alpine AS deps
RUN apk add --no-cache libc6-compat
Expand Down Expand Up @@ -55,18 +66,24 @@ ENV NODE_ENV=production
# Build shared-types first (dependency of web)
RUN pnpm --filter @pairux/shared-types build

# Build the web application
# Build the web application. The build stays on Node: Next 16.3's Turbopack
# runs PostCSS (Tailwind) in a separate `node` worker and hands it the parent
# process's flags, so under `bun --bun` with a real Node on PATH the worker
# dies with "node: --bun is not allowed in NODE_OPTIONS". What runs in
# production is the standalone server below, and that runs on Bun.
RUN pnpm --filter @pairux/web build

# Stage 3: Runner
FROM node:24-alpine AS runner
FROM alpine:3.24 AS runner
WORKDIR /app

# ffmpeg/ffprobe: AI call analysis joins the uploaded call audio, measures it
# and cuts it into pieces for transcription (src/lib/call-analysis/media.ts).
RUN apk add --no-cache ffmpeg
# libstdc++/libgcc for Bun's musl binary. ffmpeg/ffprobe: AI call analysis
# joins the uploaded call audio, measures it and cuts it into pieces for
# transcription (src/lib/call-analysis/media.ts).
RUN apk add --no-cache libstdc++ libgcc ffmpeg
COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun

# Security: Run as non-root user
# Security: Run as non-root user (same uid/gid as the Node image)
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

Expand All @@ -88,9 +105,9 @@ USER nextjs
# Expose port
EXPOSE 8080

# Health check
# Health check (Bun's fetch; 127.0.0.1, since Next binds IPv4 only)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1
CMD bun -e "fetch('http://127.0.0.1:8080/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

# Start the application
CMD ["node", "apps/web/server.js"]
# Start the standalone server under Bun
CMD ["bun", "apps/web/server.js"]
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"start": "bun --bun next start",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"typecheck": "tsc --noEmit",
Expand Down
Loading