diff --git a/docker-compose.yaml b/docker-compose.yaml index ea87a8e1e..49c3eba1a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -62,39 +62,16 @@ services: db-init: condition: service_completed_successfully - client-builder: - build: - context: . - dockerfile: docker/client-builder/Dockerfile - args: - - VITE_API_HOST=/api - container_name: lifeforge-client-builder - environment: - - DOCKER_MODE=true - - VITE_API_HOST=/api - volumes: - - ./apps:/app/apps - - ./locales:/app/locales - - client-dist:/output - depends_on: - db-init: - condition: service_completed_successfully - client: build: context: . dockerfile: docker/client/Dockerfile + args: + - VITE_API_HOST=/api container_name: lifeforge-client restart: unless-stopped ports: - "80:80" - volumes: - - client-dist:/usr/share/nginx/html depends_on: - client-builder: - condition: service_completed_successfully server: condition: service_started - -volumes: - client-dist: diff --git a/docker/client-builder/Dockerfile b/docker/client-builder/Dockerfile deleted file mode 100644 index 11984556a..000000000 --- a/docker/client-builder/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -# syntax=docker/dockerfile:1 - -# ============================================ -# Client Builder - On-demand client rebuilds -# ============================================ -FROM oven/bun:alpine - -WORKDIR /app - -# Copy source -COPY . . - -# Install all dependencies -RUN --mount=type=cache,target=/root/.bun/install/cache \ - bun install --frozen-lockfile --ignore-scripts --linker isolated - -# Set environment -ENV DOCKER_MODE=true - -# Copy entrypoint script -COPY docker/client-builder/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/client-builder/entrypoint.sh b/docker/client-builder/entrypoint.sh deleted file mode 100644 index f4f767c3b..000000000 --- a/docker/client-builder/entrypoint.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -set -e - -echo "=== LifeForge Client Builder ===" - -# Generate module registry -echo "Generating module registry..." -cd /app && bun forge modules gen-registry - -# Build client -echo "Building client..." -cd /app/client && bun run build - -# Copy to output volume -echo "Copying build to output..." -cp -r /app/client/dist/* /output/ - -echo "=== Client Build Complete ===" diff --git a/docker/client/Dockerfile b/docker/client/Dockerfile index 82dc1bcce..e99e284b1 100644 --- a/docker/client/Dockerfile +++ b/docker/client/Dockerfile @@ -1,8 +1,28 @@ # syntax=docker/dockerfile:1 # ============================================ -# Client - Static file server (Nginx only) -# Build is handled by client-builder service +# Builder stage - build client and tools +# ============================================ +FROM oven/bun:alpine AS builder + +# Accept build arg for API host +ARG VITE_API_HOST=/api +ENV VITE_API_HOST=$VITE_API_HOST + +WORKDIR /app + +# Copy source +COPY . . + +# Install dependencies +RUN --mount=type=cache,target=/root/.bun/install/cache \ + bun install --frozen-lockfile --linker isolated + +# Build client +RUN cd /app/client && bun run build + +# ============================================ +# Production stage - Nginx static server # ============================================ FROM nginx:alpine @@ -12,12 +32,12 @@ RUN apk add --no-cache curl # Copy nginx config for SPA routing and API proxying COPY docker/client/nginx.conf /etc/nginx/conf.d/default.conf -# Create directory for static files (will be mounted from volume) -RUN mkdir -p /usr/share/nginx/html +# Copy built client from builder +COPY --from=builder /app/client/dist /usr/share/nginx/html EXPOSE 80 -# Health check - verify nginx is responding +# Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost/ || exit 1 diff --git a/docker/db-init/Dockerfile b/docker/db-init/Dockerfile index 5a3791ce5..9bf2e4d8b 100644 --- a/docker/db-init/Dockerfile +++ b/docker/db-init/Dockerfile @@ -6,7 +6,28 @@ FROM ghcr.io/muchobien/pocketbase:latest AS pocketbase # ============================================ -# DB Init Container - Generates and applies migrations +# Builder stage - build forge CLI +# ============================================ +FROM oven/bun:alpine AS builder + +WORKDIR /app + +# Copy source +COPY . . + +# Install dependencies +RUN --mount=type=cache,target=/root/.bun/install/cache \ + bun install --frozen-lockfile --linker isolated + +# Build forge CLI +RUN cd /app/tools && bun run build + +# Collect only schema files to staging directory +RUN mkdir -p /schemas && \ + find /app/server/src/lib -name "schema.ts" -exec sh -c 'mkdir -p /schemas/$(dirname ${1#/app/}) && cp "$1" /schemas/$(dirname ${1#/app/})/' _ {} \; + +# ============================================ +# Production stage - minimal runtime # ============================================ FROM oven/bun:alpine @@ -15,19 +36,25 @@ COPY --from=pocketbase /usr/local/bin/pocketbase /usr/local/bin/pocketbase WORKDIR /app -# Copy source -COPY . . +# Copy ONLY bundled forge CLI (no node_modules!) +COPY --from=builder /app/tools/dist/forge.js ./forge.js -# Install all dependencies -RUN --mount=type=cache,target=/root/.bun/install/cache \ - bun install --frozen-lockfile --linker isolated +# Copy server schema files needed for migration generation +COPY --from=builder /schemas/server/src/lib ./server/src/lib + +# Copy shared package (built) for schema imports +COPY --from=builder /app/shared/dist ./shared/dist +COPY --from=builder /app/shared/package.json ./shared/package.json + +# Install minimal dependencies for schema evaluation +RUN echo '{"dependencies":{"zod":"^4.0.0"}}' > package.json && bun install # Set environment for Docker mode ENV DOCKER_MODE=true ENV PB_DIR=/pb_data ENV PB_BINARY_PATH=/usr/local/bin/pocketbase -# Copy entrypoint script +# Copy entrypoint COPY docker/db-init/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/docker/db-init/entrypoint.sh b/docker/db-init/entrypoint.sh index e47ede05d..25f605c98 100644 --- a/docker/db-init/entrypoint.sh +++ b/docker/db-init/entrypoint.sh @@ -7,8 +7,8 @@ echo "Generating database migrations..." # Ensure the migrations directory exists mkdir -p /pb_data/pb_migrations -# Generate migrations -cd /app && bun run forge db push +# Generate and apply migrations using bundled forge CLI +cd /app && bun forge.js db push -echo "Migrations generated successfully!" +echo "Migrations applied successfully!" echo "=== DB Init Complete ===" diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index ef13f8127..ac914f109 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 # ============================================ -# Builder stage - contains build dependencies +# Builder stage - build server bundle # ============================================ FROM oven/bun:alpine AS builder @@ -9,18 +9,24 @@ RUN apk update && apk add git WORKDIR /lifeforge -# Copy all files +# Copy source COPY . . -# Install all dependencies for build +# Install dependencies RUN --mount=type=cache,target=/root/.bun/install/cache \ bun install --frozen-lockfile --linker isolated -# Build types (gen-registry runs at runtime now via bun start) -RUN cd /lifeforge/server && bun run types +# Build server bundle +RUN cd /lifeforge/server && bun run build + +# Create cleaned package.json without workspace deps for production install +RUN bun -e "const pkg = require('./server/package.json'); \ + delete pkg.dependencies.shared; \ + delete pkg.devDependencies; \ + require('fs').writeFileSync('./server/package.docker.json', JSON.stringify(pkg, null, 2))" # ============================================ -# Production stage - minimal runtime image +# Production stage - minimal runtime # ============================================ FROM oven/bun:alpine AS production @@ -29,18 +35,21 @@ RUN apk add --no-cache curl WORKDIR /lifeforge -# Copy only necessary files from builder -# Note: apps/ and locales/ are mounted as volumes, not copied -COPY --from=builder /lifeforge/node_modules ./node_modules -COPY --from=builder /lifeforge/server ./server -COPY --from=builder /lifeforge/shared ./shared -COPY --from=builder /lifeforge/packages ./packages -COPY --from=builder /lifeforge/tools ./tools -COPY --from=builder /lifeforge/package.json ./package.json -COPY --from=builder /lifeforge/bun.lock ./bun.lock -COPY --from=builder /lifeforge/tsconfig.json ./tsconfig.json +# Copy ONLY bundled server (no node_modules!) +COPY --from=builder /lifeforge/server/dist ./server/dist -# Copy entrypoint script +# Copy server source for @functions imports (modules import from @functions/*) +COPY --from=builder /lifeforge/server/src ./server/src + +# Copy shared package for module imports +COPY --from=builder /lifeforge/shared/dist ./shared/dist +COPY --from=builder /lifeforge/shared/package.json ./shared/package.json + +# Install server dependencies for module loading (using cleaned package.json without workspace deps) +COPY --from=builder /lifeforge/server/package.docker.json ./package.json +RUN bun install --production + +# Copy entrypoint COPY docker/server/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/docker/server/entrypoint.sh b/docker/server/entrypoint.sh index e998c076d..825ac14bc 100644 --- a/docker/server/entrypoint.sh +++ b/docker/server/entrypoint.sh @@ -8,7 +8,50 @@ until wget -q --spider http://db:8090/api/health 2>/dev/null; do done echo "PocketBase is ready!" -# Start the server +# Create symlinks for server path aliases so modules can resolve @functions/*, @lib/*, etc. +mkdir -p /lifeforge/node_modules +ln -sf /lifeforge/server/src/core/functions /lifeforge/node_modules/@functions +ln -sf /lifeforge/server/src/lib /lifeforge/node_modules/@lib +ln -sf /lifeforge/server/src/core /lifeforge/node_modules/@core +ln -sf /lifeforge/server/src/core/constants.ts /lifeforge/node_modules/@constants +ln -sf /lifeforge/server/src/core/schema /lifeforge/node_modules/@schema +ln -sf /lifeforge/shared /lifeforge/node_modules/shared + +# Install module-specific dependencies (skip workspace deps that fail) +echo "Installing module dependencies..." +for dir in /lifeforge/apps/*/; do + if [ -f "${dir}package.json" ]; then + modname=$(basename "$dir") + # Only install if node_modules doesn't exist or is empty + if [ ! -d "${dir}node_modules" ] || [ -z "$(ls -A ${dir}node_modules 2>/dev/null)" ]; then + echo "Installing deps for $modname..." + # Create temp package.json without workspace deps, install, then restore + cd "$dir" + if [ -f package.json ]; then + # Remove workspace deps before install + bun -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); + const original = JSON.stringify(pkg, null, 2); + fs.writeFileSync('package.json.bak', original); + if (pkg.dependencies) { + for (const [k,v] of Object.entries(pkg.dependencies)) { + if (v.startsWith('workspace:')) delete pkg.dependencies[k]; + } + } + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)); + " 2>/dev/null || true + bun install --production 2>/dev/null || true + # Restore original package.json + if [ -f package.json.bak ]; then + mv package.json.bak package.json + fi + fi + fi + fi +done +echo "Module dependencies installed." + echo "Starting server..." cd /lifeforge/server -exec bun run start +exec bun dist/server.js