Rewrite SAAC_DEPLOYMENT.md to lead with the two-domain model: - Production (yourapp.<server>.domain.com) — updates on saac deploy - Hot-reload (yourapp-hot.<server>.domain.com) — auto-rebuilds on git push Added: recommended dev workflow (push → check hot → deploy to prod), nodemon.json explanation, development cycle diagram, customization guide. Updated Dockerfile and docker-compose.yml headers to explain which container uses which file and reference nodemon.json.
8.3 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.
Your Two Domains
Every app gets two live URLs on the server. Both are created automatically when you deploy:
yourapp.<server>.startanaicompany.com ← PRODUCTION (customers see this)
yourapp-hot.<server>.startanaicompany.com ← HOT-RELOAD (you develop with this)
For example, if your app domain is my-saas.adam.startanaicompany.com:
- Production:
https://my-saas.adam.startanaicompany.com - Hot-reload:
https://my-saas-hot.adam.startanaicompany.com
How to Use Them
Hot-reload (-hot domain) — Use this while developing. Every git push automatically:
- Pulls your latest code
- Rebuilds the React frontend (
npm run build) - Restarts the Express server
- Your changes are live in ~10-20 seconds
Recommended development workflow:
# 1. Make your code changes
# 2. Push to git
git add . && git commit -m "Add login page" && git push
# 3. Wait ~15 seconds, then check the hot domain
curl https://yourapp-hot.adam.startanaicompany.com/health
# 4. Once it works on hot, deploy to production
saac deploy
# 5. Verify production
curl https://yourapp.adam.startanaicompany.com/health
Production domain — Only updates when you run saac deploy. This rebuilds the Docker image from your Dockerfile and restarts everything. Use this for the final, tested version.
Why Two Containers?
| Container | Domain | How it runs your code | When it updates |
|---|---|---|---|
| Production | yourapp.example.com |
Built from your Dockerfile (immutable Docker image) |
Only on saac deploy |
| Hot-reload | yourapp-hot.example.com |
Volume-mounts your git repo, runs npm run dev via nodemon |
Automatically on every git push |
Key differences:
- Production uses your Dockerfile. The Docker image is built once and runs until next deploy.
- Hot-reload does NOT use your Dockerfile. It mounts your source code directly, installs dependencies, builds the React client, and runs Express with nodemon.
- Hot-reload auto-rebuilds everything —
nodemon.jsonwatches bothserver.jsandclient/src/. When files change (aftergit push), it runsnpm run build(React) and restartsnode server.js.
How nodemon.json Works
The nodemon.json file in your repo tells the hot-reload container what to watch:
{
"watch": ["server.js", "client/src"],
"ext": "js,ts,tsx,jsx,css,json",
"exec": "npm run build && node server.js"
}
- watch: Directories/files to monitor for changes
- ext: File extensions that trigger a rebuild
- exec: What to run when changes are detected — rebuilds React, restarts Express
You can customize this. For example, if you add a lib/ directory with shared code:
{
"watch": ["server.js", "client/src", "lib"],
"ext": "js,ts,tsx,jsx,css,json",
"exec": "npm run build && node server.js"
}
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
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/postgres
postgres:
environment:
- POSTGRES_DB=postgres # 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, push, check the hot domain
- When it works, add the next feature
- 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
Quick Reference: Development Cycle
git push ──→ hot-reload updates (~15s) ──→ check yourapp-hot domain
│ │
│ ┌─────── works? ──┘
│ │
│ YES ────┤──── NO
│ │ │ │
│ saac deploy │ fix code, git push again
│ │ │
│ production │
│ updated │
└────────────────────────────┘
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" on production | Run saac deploy to rebuild the Docker image |
| "App serves old code" on hot domain | Wait ~15s after push. Check saac logs for rebuild errors |
| Hot-reload container crashes | Check saac logs — usually a missing dependency or build error |
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.