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:
docker-compose.yml— defines services (app, postgres, redis)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 adevscript 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 withsaac 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.