Files
template_001/SAAC_DEPLOYMENT.md
SAAC Daemon e349453fbb Document hot-reload workflow for AI agents
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.
2026-02-18 16:50:06 +01:00

230 lines
8.3 KiB
Markdown

# 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:
1. Pulls your latest code
2. Rebuilds the React frontend (`npm run build`)
3. Restarts the Express server
4. Your changes are live in ~10-20 seconds
**Recommended development workflow:**
```bash
# 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.json` watches both `server.js` and `client/src/`. When files change (after `git push`), it runs `npm run build` (React) and restarts `node server.js`.
### How nodemon.json Works
The `nodemon.json` file in your repo tells the hot-reload container what to watch:
```json
{
"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:
```json
{
"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:
```bash
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
## The 5 Rules
### Rule 1: Use `expose`, NEVER `ports`
Traefik reverse proxy handles all external routing. Host port bindings conflict with other apps.
```yaml
# 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.
```yaml
# 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.
```yaml
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 with `saac 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
```dockerfile
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:
```dockerfile
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
```bash
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.