feat: refactor Docker setup and improve build process (WIP)

This commit is contained in:
Melvin Chia
2026-01-12 16:18:43 +08:00
parent 3ffb4cc136
commit 62e81fb24c
8 changed files with 135 additions and 101 deletions

View File

@@ -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:

View File

@@ -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"]

View File

@@ -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 ==="

View File

@@ -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

View File

@@ -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

View File

@@ -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 ==="

View File

@@ -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

View File

@@ -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