Files
hireflow/SAAC_DEPLOYMENT.md

5.9 KiB

SAAC Deployment — How Your App Gets Built

READ THIS FIRST. This file explains exactly how SAAC deploys your app. Understanding this will save you hours of debugging.

The Build Process

When you run saac deploy, the daemon executes these exact commands in your repo:

docker compose -f docker-compose.yml -f docker-compose.saac.yml build
docker compose -p saac-{uuid} -f docker-compose.yml -f docker-compose.saac.yml up -d

Your docker-compose.yml and Dockerfile ARE used. The auto-generated docker-compose.saac.yml overlay ADDS labels, networks, and restart policy — it never replaces your config.

Required Files

Your repo MUST have:

  1. docker-compose.yml — defines services (app, postgres, redis)
  2. Dockerfile — referenced by the build directive in compose

CRITICAL: Two Containers Per App

Every app gets TWO containers. Understanding this prevents the #1 debugging confusion:

Container Domain How it runs your code When it updates
Production yourapp.startanaicompany.com Built from your Dockerfile (immutable image) Only on saac deploy
Hot-reload yourapp-hot.startanaicompany.com Volume-mounts your git repo, runs npm run dev Instantly on git push

Key facts:

  • Production runs the Docker image you built. Your Dockerfile, CMD, and build steps all apply.
  • Hot-reload does NOT use your Dockerfile. It mounts your source code directly and runs nodemon/npm run dev.
  • Your main domain routes to PRODUCTION, not hot-reload. If production works but hot-reload doesn't, your app is fine.
  • TypeScript projects: Production container compiles TS in the Dockerfile (RUN npm run build). Hot-reload needs a dev script in package.json that compiles and runs (e.g., "dev": "tsc && node dist/server.js" or "dev": "tsx watch src/server.ts").

Common confusion: "My Docker build works but the app serves old code!" — You're probably looking at the hot-reload container. Check the PRODUCTION domain (without -hot).

The 5 Rules

Rule 1: Use expose, NEVER ports

Traefik reverse proxy handles all external routing. Host port bindings conflict with other apps.

# WRONG — will conflict with other apps on the server
ports:
  - "3000:3000"

# CORRECT — Traefik routes traffic to this port
expose:
  - "3000"

Rule 2: Database host = service name, NOT localhost

In Docker Compose, services talk to each other by service name.

# WRONG — localhost means "inside this container" in Docker
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mydb

# CORRECT — "postgres" is the service name in docker-compose.yml
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/mydb

Rule 3: Database name must match between app and postgres service

The POSTGRES_DB in your postgres service creates the database. Your app must connect to the SAME name.

services:
  app:
    environment:
      # Must match POSTGRES_DB below!
      - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/template_db
  postgres:
    environment:
      - POSTGRES_DB=template_db  # This creates the database

WARNING: If you change POSTGRES_DB after first deploy, the old name persists in the Docker volume. The new name won't exist. Either keep the original name or destroy and recreate the postgres volume.

Rule 4: Keep it simple — iterate incrementally

Start with the working template, then add features one at a time.

DO NOT:

  • Replace Express with TypeScript + Prisma + monorepo in one commit
  • Create complex multi-stage Dockerfiles before the basic app works
  • Have multiple agents push changes simultaneously (causes deploy loops)

DO:

  • Get the template deploying first (saac deploy, verify with saac logs)
  • Add one feature, deploy, verify
  • Add the next feature, deploy, verify
  • Coordinate deploys — only ONE agent should push/deploy at a time

Rule 5: Dockerfile must produce a running container

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# If TypeScript: RUN npm run build
CMD ["node", "server.js"]

For TypeScript projects:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install          # Install ALL deps (including devDependencies for tsc)
COPY . .
RUN npm run build        # Compile TypeScript
RUN npm prune --production  # Remove devDeps from final image
CMD ["node", "dist/server.js"]

Environment Variables

Env vars set via saac env set KEY=VALUE are written to .env in your repo directory before build.

Debugging Commands

saac logs                              # Runtime logs (production container)
saac logs --type build                 # Build/deploy logs
saac exec "ls -la"                     # Run command in production container
saac exec "cat package.json"           # Check what's in the container
saac db sql "SELECT * FROM users LIMIT 5"  # Query database

Common Mistakes and Fixes

Mistake Fix
ports: "3000:3000" Change to expose: ["3000"]
DB_HOST=localhost Change to DB_HOST=postgres (service name)
Changed POSTGRES_DB name after first deploy Keep original name or delete postgres volume
tsc: not found in Dockerfile Install ALL deps first: RUN npm install (not --production)
.dockerignore has dist/ Remove it — dist is built inside container
Multiple agents deploying simultaneously Coordinate — one agent deploys at a time
"App serves old code" Check production domain (not hot-reload). Run saac deploy to rebuild.
Hot-reload container crashes This is separate from production. Fix package.json dev script.

Do NOT Add Traefik Labels

The SAAC daemon handles Traefik routing automatically via file provider. Traefik Docker labels in your docker-compose.yml are ignored. You can remove them.