Deployment & Docker
Build Patterns
This document describes the build patterns used for FlowState Docker images.
Overview
FlowState services use optimized multi-stage Docker builds with esbuild bundling. The goal is to produce small, efficient images (~200-500MB) compared to naive approaches that can produce 20GB+ images.
Build Architecture
Two-Stage Pattern
All service Dockerfiles follow this pattern:
Stage 1: Builder (node:20-slim)
├── Install build tools (git, python3, build-essential)
├── Clone repository from GitHub
├── Install all dependencies (yarn install)
├── Build with Nx
├── Bundle with esbuild
└── Install external module dependencies
Stage 2: Production (node:20-slim)
├── Install curl (for health checks)
├── Copy bundled server.js
├── Copy node_modules (external deps only)
├── Create non-root user
└── Configure health check
esbuild Bundling
We use esbuild to bundle most dependencies into a single server.js file (~1-5MB). This eliminates the need to copy the entire node_modules directory (~3GB in a monorepo).
RUN npx esbuild packages/my-service/dist/index.js \
--bundle \
--platform=node \
--target=node20 \
--outfile=/app/bundle/server.js \
--minify \
--sourcemap \
--external:bcrypt # Native modules must be external
Handling External Dependencies
Some modules cannot be bundled with esbuild:
| Module Type | Reason | Solution |
|---|---|---|
| Native modules (bcrypt) | Contains compiled C++ bindings | Mark external, install via npm |
| Dynamic imports (rxdb) | Uses require() at runtime for plugins | Mark external, install via npm |
Pattern: Installing External Dependencies
Instead of manually copying node_modules directories (which misses transitive deps), use npm to install:
# Get the version from the monorepo
RUN BCRYPT_VERSION=$(node -p "require('./node_modules/bcrypt/package.json').version") && \
cd /app/bundle && \
echo "{\"name\":\"deps\",\"private\":true,\"dependencies\":{\"bcrypt\":\"$BCRYPT_VERSION\"}}" > package.json && \
npm install --omit=dev
This ensures all transitive dependencies are included.
Service-Specific Patterns
Services with bcrypt (auth-server, mcp-http)
# Bundle everything except bcrypt
RUN npx esbuild ... --external:bcrypt
# Install bcrypt with all its dependencies
RUN BCRYPT_VERSION=$(node -p "require('./node_modules/bcrypt/package.json').version") && \
cd /app/bundle && \
echo "{\"dependencies\":{\"bcrypt\":\"$BCRYPT_VERSION\"}}" > package.json && \
npm install --omit=dev
Services with RxDB (rxdb-server)
RxDB uses dynamic imports for plugins, so it must be external:
# Bundle everything except rxdb
RUN npx esbuild ... \
--external:rxdb \
--external:rxdb/* \
--external:rxdb-premium \
--external:rxdb-premium/*
# Install rxdb with all dependencies
RUN --mount=type=secret,id=rxdb_premium \
RXDB_VERSION=$(node -p "require('./node_modules/rxdb/package.json').version") && \
cd /app/bundle && \
echo "{\"dependencies\":{\"rxdb\":\"$RXDB_VERSION\"}}" > package.json && \
npm config set //npm.rxdb.info/:_authToken "$(cat /run/secrets/rxdb_premium)" && \
npm install --omit=dev
Services with Static Assets (auth-server)
Some services need additional directories copied:
# In production stage
COPY --from=builder /app/packages/flowstate-auth-server/templates /templates
COPY --from=builder /app/packages/flowstate-auth-server/views /views
Services with Pure JS (obs-server, rag-sync, document-store)
These services have no native modules and can be fully bundled:
RUN npx esbuild packages/my-service/dist/index.js \
--bundle \
--platform=node \
--target=node20 \
--outfile=/app/bundle/server.js \
--minify \
--sourcemap
No external dependencies needed - results in smallest images (~200MB).
Image Sizes
| Service | Size | External Deps |
|---|---|---|
| obs-server | ~217MB | None |
| document-store | ~225MB | None |
| rag-sync | ~237MB | None |
| auth-server | ~371MB | bcrypt |
| mcp-http | ~383MB | bcrypt |
| rxdb-server | ~969MB | rxdb, rxdb-premium |
Common Issues
"Cannot find module X"
Cause: A dependency was marked as external but not installed in the production image.
Solution: Add the dependency to the npm install step:
RUN echo "{\"dependencies\":{\"missing-module\":\"^1.0.0\"}}" > package.json && \
npm install --omit=dev
"Cannot find module '@mapbox/node-pre-gyp'"
Cause: bcrypt is bundled but it requires node-pre-gyp at runtime.
Solution: Mark bcrypt as external and install it via npm (see pattern above).
Missing templates/views/assets
Cause: Static assets aren't included in the esbuild bundle.
Solution: Copy them explicitly in the production stage:
COPY --from=builder /app/packages/my-service/assets /assets
Build uses cached old code
Cause: Docker caches the git clone step.
Solution: Use --no-cache flag:
./docker/build.sh local my-service --no-cache
Build Commands
# Build single service
./docker/build.sh local rxdb
# Build with no cache (after pushing code changes)
./docker/build.sh local rxdb --no-cache
# Build all services
./docker/build.sh local all
# Build from specific branch
./docker/build.sh local rxdb --branch feature-xyz
Debugging Builds
Check what's in the bundle
# Run a temporary container
docker run --rm -it flowstate-my-service:latest sh
# List files
ls -la /app/
ls -la /app/node_modules/
Check bundle size
docker images | grep flowstate
View build logs
# Build with progress output
DOCKER_BUILDKIT=1 docker build --progress=plain ...
Adding a New Service
- Copy an existing Dockerfile as a template
- Update package name in
yarn nx buildcommand - Update entry point in esbuild command
- Identify any native modules and mark them external
- Add npm install step for external dependencies
- Update port and health check endpoint
- Test the build:
./docker/build.sh local my-service --no-cache - Verify the container runs:
docker run --rm flowstate-my-service:latest