Skip to content

Production Deployment

This guide covers deploying Flowstate to production using Docker images hosted on a container registry. The platform uses a multi-stage build process with optimized layer caching for fast, reliable deployments.

Flowstate production architecture consists of:

ServiceRuntimeDescriptionPublic Access
BackendNode.js 22Express REST API (internal only)No
GatewayNode.js 22Connect RPC service (public)Yes
WebNode.js 22 SSRAstro standalone server (main app)Yes
AdminNode.js 22 SSRAstro standalone server (admin dashboard)Yes
MongoDBMongoDB 7Replica set databaseNo
RedisRedis 7 AlpineCache and pub/subNo
StorageS3-compatibleObject storage (AWS S3, DO Spaces, R2)Yes (public URLs)

Network topology:

Internet
├──> Web (app.your-domain.com) ──┐
├──> Admin (admin.your-domain.com) ──┼──> Backend (internal) ──┬──> MongoDB
├──> Gateway (rpc.your-domain.com) ──┘ └──> Redis
└──> Storage (storage.your-domain.com)

Production images are built via GitHub Actions and pushed to your container registry:

ImageDescription
BackendREST API server
WebAstro web frontend
AdminAdmin dashboard
DocsDocumentation site

Note: The Gateway does not have its own image. It reuses the backend image with command: ['node', 'dist/gateway.js'] in the production compose file.

Tags:

  • latest — Latest build from production branch
  • staging — Latest build from staging branch
  • main — Latest build from main branch
  • dev — Latest build from dev branch
  • v1.2.3 — Specific version tag

All images use multi-stage builds for optimization:

# Stage 1: Dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# Stage 2: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
# Stage 3: Production
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]

Benefits:

  • Smaller final image (no build tools)
  • Faster deployments (fewer layers to pull)
  • Better security (minimal attack surface)

The build process uses dual-layer caching for maximum speed:

GitHub Actions pushes cache layers to the container registry:

- name: Build and push
uses: docker/build-push-action@v5
with:
cache-from: type=registry,ref=your-registry/flowstate/backend:buildcache
cache-to: type=registry,ref=your-registry/flowstate/backend:buildcache,mode=max

pnpm store is cached between workflow runs:

- name: Cache pnpm store
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}

Layers are ordered from least to most frequently changed:

# 1. Base image (changes rarely)
FROM node:22-alpine
# 2. System dependencies (changes rarely)
RUN apk add --no-cache curl
# 3. Package files (changes occasionally)
COPY package.json pnpm-lock.yaml ./
# 4. Dependencies (changes occasionally)
RUN pnpm install --frozen-lockfile
# 5. Source code (changes frequently)
COPY . .
# 6. Build (changes frequently)
RUN pnpm build

Flowstate uses a four-tier branching model:

dev → main → staging → production
BranchPurposeAuto-DeployImage Tag
devActive developmentNodev
mainStable, tested codeNomain
stagingPre-production validationYes (staging env)staging
productionLive releaseYes (production env)latest, v1.2.3

Promotion flow:

  1. Feature branches merge to dev via PR
  2. dev merges to main when stable (weekly)
  3. main merges to staging for pre-production testing
  4. staging merges to production after validation

Never skip tiers — always promote through each environment.

GitHub Actions builds and pushes images on every push to dev, main, staging, or production:

name: Build and Push Docker Images
on:
push:
branches: [dev, main, staging, production]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
service: [backend, web, admin, docs]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: your-registry
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
file: ./apps/${{ matrix.service }}/Dockerfile
push: true
tags: your-registry/flowstate/${{ matrix.service }}:${{ github.ref_name }}
cache-from: type=registry,ref=your-registry/flowstate/${{ matrix.service }}:buildcache
cache-to: type=registry,ref=your-registry/flowstate/${{ matrix.service }}:buildcache,mode=max

Build time:

  • Cold build (no cache): ~8 minutes
  • Warm build (with cache): ~2 minutes

For small deployments on a single server:

version: '3.8'
services:
backend:
image: your-registry/flowstate/backend:latest
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongodb
- redis
restart: unless-stopped
web:
image: your-registry/flowstate/web:latest
environment:
- SERVER_API_URL=http://backend:3000
ports:
- '80:4321'
depends_on:
- backend
restart: unless-stopped
mongodb:
image: mongo:7
command: --replSet rs0
volumes:
- mongodb_data:/data/db
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
mongodb_data:
redis_data:

Deploy:

Terminal window
# Pull latest images
docker compose pull
# Start services
docker compose up -d
# View logs
docker compose logs -f

For multi-node deployments with load balancing:

version: '3.8'
services:
backend:
image: your-registry/flowstate/backend:latest
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
networks:
- flowstate
web:
image: your-registry/flowstate/web:latest
deploy:
replicas: 2
ports:
- '80:4321'
networks:
- flowstate
networks:
flowstate:
driver: overlay

Deploy:

Terminal window
# Initialize swarm
docker swarm init
# Deploy stack
docker stack deploy -c docker-compose.prod.yml flowstate
# Scale services
docker service scale flowstate_backend=5
# Update service
docker service update --image your-registry/flowstate/backend:v1.2.3 flowstate_backend

For large-scale deployments with auto-scaling:

apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: your-registry/flowstate/backend:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: flowstate-secrets
key: database-url
resources:
requests:
memory: '512Mi'
cpu: '500m'
limits:
memory: '1Gi'
cpu: '1000m'
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5

Deploy:

Terminal window
# Apply manifests
kubectl apply -f k8s/
# Check status
kubectl get pods
kubectl get services
# View logs
kubectl logs -f deployment/backend
# Scale
kubectl scale deployment backend --replicas=5

All services expose a health endpoint for monitoring:

ServiceEndpointDescription
Backend/healthChecks MongoDB + Redis connectivity
Web/healthProxies to backend health check
Admin/healthProxies to backend health check
Gateway/healthLightweight liveness probe
Docs/healthzNginx liveness probe (static site)

Backend health response:

{
"status": "ok",
"timestamp": "2026-02-28T10:30:00.000Z",
"services": {
"database": { "status": "healthy", "latency": 12 },
"redis": { "status": "healthy", "latency": 3 },
"storage": { "status": "healthy", "latency": 45 }
},
"uptime": 86400
}

All images run as the node user (UID 1000):

USER node

Never run containers as root in production.

Mount the root filesystem as read-only:

services:
backend:
read_only: true
tmpfs:
- /tmp
- /app/.cache

Never commit secrets to the repository. Use environment variables or secret management tools:

  • Docker Compose: .env file (gitignored)
  • Docker Swarm: docker secret create
  • Kubernetes: kubectl create secret
  • Cloud providers: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault

Only expose necessary ports:

services:
backend:
# No ports exposed (internal only)
web:
ports:
- '80:4321' # Only web port exposed

Centralize logs using a logging driver:

services:
backend:
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '3'

Production logging solutions:

  • Loki + Grafana — Self-hosted
  • Datadog — SaaS
  • CloudWatch Logs — AWS
  • Stackdriver — GCP

Expose Prometheus metrics from the backend /metrics endpoint and scrape with Prometheus:

scrape_configs:
- job_name: 'flowstate-backend'
static_configs:
- targets: ['backend:3000']

Set up alerts for critical issues:

  • Service down (health check fails)
  • High error rate (>5% of requests)
  • High latency (p95 >1s)
  • Database connection failures
  • Redis connection failures
  • Disk space low (<10% free)

Automated daily backups:

#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/mongodb"
docker compose exec -T mongodb mongodump --archive > "$BACKUP_DIR/backup_$DATE.archive"
# Keep only last 7 days
find "$BACKUP_DIR" -name "backup_*.archive" -mtime +7 -delete

Restore:

Terminal window
docker compose exec -T mongodb mongorestore --archive < backup_20260228_103000.archive

Redis uses AOF (Append-Only File) for persistence:

services:
redis:
command: redis-server --appendonly yes
volumes:
- redis_data:/data

Backup:

Terminal window
docker compose exec redis redis-cli BGSAVE
docker compose cp redis:/data/dump.rdb ./backup/

S3-compatible storage handles replication automatically. For additional safety:

  • Enable versioning on buckets
  • Configure lifecycle policies
  • Set up cross-region replication

Desktop builds are triggered by pushing a version tag:

Terminal window
git tag v1.2.3
git push origin v1.2.3

GitHub Actions builds cross-platform Electron apps:

PlatformArtifactSize
macOS (Intel)Flowstate-1.2.3-x64.dmg~150MB
macOS (Apple Silicon)Flowstate-1.2.3-arm64.dmg~150MB
WindowsFlowstate-Setup-1.2.3.exe~120MB
Linux (AppImage)Flowstate-1.2.3.AppImage~140MB

Auto-update configuration:

import { autoUpdater } from 'electron-updater';
autoUpdater.setFeedURL({
provider: 's3',
bucket: process.env.DESKTOP_S3_BUCKET,
region: '<region>',
});
autoUpdater.checkForUpdatesAndNotify();

If a deployment fails, rollback to the previous version:

Docker Compose:

Terminal window
# Update docker-compose.yml to use the previous tag, then restart
docker compose up -d

Docker Swarm:

Terminal window
docker service update --image your-registry/flowstate/backend:v1.2.2 flowstate_backend

Kubernetes:

Terminal window
kubectl rollout undo deployment/backend
ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-old-space-size=2048"
services:
mongodb:
command: --wiredTigerCacheSizeGB 2 --replSet rs0
services:
redis:
command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru
services:
backend:
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M

Before deploying to production:

  • All environment variables configured
  • Secrets stored securely (not in code)
  • Health checks passing
  • Database backups configured
  • Monitoring and alerts set up
  • SSL/TLS certificates installed
  • CORS origins restricted (not *)
  • Rate limiting enabled
  • Logs centralized
  • Rollback plan documented
  • Load testing completed
  • Security audit passed
  • Documentation updated
Terminal window
# Authenticate with your container registry
echo $REGISTRY_TOKEN | docker login your-registry -u USERNAME --password-stdin
# Pull manually
docker pull your-registry/flowstate/backend:latest
Terminal window
# Check logs
docker compose logs backend
# Check health
docker compose ps
# Restart service
docker compose restart backend
Terminal window
# Verify MongoDB is running
docker compose ps mongodb
# Check replica set status
docker compose exec mongodb mongosh --eval "rs.status()"
# Verify connection string (check for typos or missing replicaSet param)
echo $DATABASE_URL
Terminal window
# Check container stats
docker stats
# Increase memory limits in your compose or deployment manifest

For deployment issues:

  1. Check logs: docker compose logs -f
  2. Verify health endpoints are returning 200
  3. Review documentation: /docs/deployment
  4. Contact DevOps team: [email protected]