--- name: ec2-deploy description: Deploy to EC2 production server. Use when deploying new code, rebuilding Docker images, wiping and reseeding the database, checking container health, or managing the EC2 instance via AWS CLI. --- # EC2 Deploy Skill ## ⚠️💣 MANDATORY CONSENT CHECK 💣⚠️ **Check if `.claude/user-consent.md` exists with `ACCEPTED: true`.** If yes, display: `✅ Prior consent on file (DATE). Proceeding.` and skip to "Config." If not, present the 3 warnings from `.claude/skills/instruction-tuning/SKILL.md` (ToS, autonomous agents, resource consumption). All 3 must be accepted. Write `.claude/user-consent.md` on acceptance. This file is shared across all skills that affect external infrastructure. --- Manage EC2 instances and Docker Compose services using the AWS CLI and SSH. **Use this skill when asked to:** - Deploy new code to production - Wipe and reseed the production database - Restart/rebuild containers - Check container health, logs, or DB state - Find the EC2 instance IP or connection info --- ## Config Configure these values based on your deployment environment. All commands below reference them by name. | Variable | Description | Example | |----------|-------------|---------| | `INSTANCE_NAME` | EC2 instance name tag | `my-app-prod` | | `SSH_KEY` | Local SSH private key | `~/.ssh/my-app-prod-key.pem` | | `SSH_USER` | OS user on instance | `ubuntu` (or `ec2-user`) | | `PROJECT_DIR` | Git repo root on instance | `/home/ubuntu/my-project` | | `COMPOSE_FILE` | Docker Compose config | `docker-compose.prod.yml` | | `DB_USER` | Database username | `appuser` | | `DB_NAME` | Database name | `app_db` | Get your `INSTANCE_ID` from AWS console or `aws ec2 describe-instances --filters "Name=tag:Name,Values=$INSTANCE_NAME"`. --- ## Instance Discovery **ALWAYS start here.** Never hardcode IPs — use AWS CLI to get the current public IP: ```bash aws ec2 describe-instances \ --filters "Name=instance-state-name,Values=running" \ --query "Reservations[*].Instances[*].{ID:InstanceId,Name:Tags[?Key=='Name']|[0].Value,IP:PublicIpAddress,Type:InstanceType,State:State.Name}" \ --output table ``` Find your instance name from the output above. If its public IP has changed, this command will always show the current one. --- ## SSH Connection ```bash ssh -i $SSH_KEY -o StrictHostKeyChecking=no $SSH_USER@ ``` - **Key file**: `$SSH_KEY` (configure before use) - **Username**: `$SSH_USER` (typically `ubuntu` for Ubuntu, `ec2-user` for Amazon Linux) - **Project root**: `$PROJECT_DIR` (your git repo path) Verify the key before SSHing: ```bash aws ec2 describe-instances \ --instance-ids $INSTANCE_ID \ --query "Reservations[0].Instances[0].KeyName" \ --output text ``` --- ## Container Architecture Docker containers managed via `docker-compose.prod.yml`: | Container | Image | Purpose | |-----------|-------|---------| | `app_db` | timescale/timescaledb or postgres | PostgreSQL database | | `app_redis` | redis:7-alpine | Job queue (BullMQ) | | `app_api` | your-app-api (local build) | API server | | `app_web` | your-app-web (local build) | Frontend (Next.js or similar) | | `app_proxy` | caddy:2-alpine | Reverse proxy (80/443) | **Key facts:** - Database is NOT exposed to host ports in production — only accessible via `docker exec` or within Docker network - `.env` file is at `$PROJECT_DIR/.env` (not in the Docker image) - Adjust container names to match your `docker-compose.prod.yml` --- ## Routine Deployment (code only, no DB wipe) When only code changes (no schema or seed changes): ```bash ssh -i $SSH_KEY $SSH_USER@ " cd $PROJECT_DIR git pull origin main docker compose -f $COMPOSE_FILE build api web docker compose -f $COMPOSE_FILE up -d " ``` --- ## Full Wipe + Reseed Deployment When schema changed or a clean slate is needed. ### Step 1: Ensure local commit is pushed ```bash git push origin main ``` ### Step 2: Pull on EC2 ```bash ssh -i $SSH_KEY $SSH_USER@ " cd $PROJECT_DIR && git pull origin main && git log --oneline -3 " ``` ### Step 3: Drop and recreate the database ```bash ssh -i $SSH_KEY $SSH_USER@ " docker exec app_db psql -U $DB_USER -d postgres \ -c \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$DB_NAME' AND pid<>pg_backend_pid();\" docker exec app_db psql -U $DB_USER -d postgres \ -c 'DROP DATABASE IF EXISTS $DB_NAME;' docker exec app_db psql -U $DB_USER -d postgres \ -c 'CREATE DATABASE $DB_NAME OWNER $DB_USER;' " ``` ### Step 4: Stop api and web (keep db + redis running) ```bash ssh -i $SSH_KEY $SSH_USER@ " cd $PROJECT_DIR docker compose -f $COMPOSE_FILE stop api web " ``` ### Step 5: Rebuild images ```bash ssh -i $SSH_KEY $SSH_USER@ " cd $PROJECT_DIR docker compose -f $COMPOSE_FILE build api web " ``` Takes 3–8 minutes depending on dependency installations. ### Step 6: Run migrations and seed data ```bash ssh -i $SSH_KEY $SSH_USER@ " cd $PROJECT_DIR docker compose -f $COMPOSE_FILE run --rm --no-deps api \ sh -c 'cd /app && pnpm run db:migrate && pnpm run db:seed' " ``` Adjust migration/seed commands to match your project setup. ### Step 7: Start all containers ```bash ssh -i $SSH_KEY $SSH_USER@ " cd $PROJECT_DIR docker compose -f $COMPOSE_FILE up -d " ``` ### Step 8: Verify ```bash # Container health ssh -i $SSH_KEY $SSH_USER@ \ "docker ps --format 'table {{.Names}}\t{{.Status}}'" # API health curl -s https://your-domain.com/api/health # DB check ssh -i $SSH_KEY $SSH_USER@ " docker exec app_db psql -U $DB_USER -d $DB_NAME -c 'SELECT COUNT(*) FROM information_schema.tables;' " ``` --- ## Running Ad-Hoc Commands ### Execute SQL in the DB ```bash ssh -i $SSH_KEY $SSH_USER@ \ "docker exec app_db psql -U $DB_USER -d $DB_NAME -c 'SELECT COUNT(*) FROM users;'" ``` ### View container logs ```bash ssh -i $SSH_KEY $SSH_USER@ \ "docker logs app_api --tail 50 -f" ``` ### Run a script in the api container ```bash ssh -i $SSH_KEY $SSH_USER@ " cd $PROJECT_DIR docker compose -f $COMPOSE_FILE run --rm --no-deps api \ sh -c 'pnpm run scripts/my-script.ts' " ``` --- ## Debugging Common Issues | Issue | Likely Cause | Fix | |-------|------------|-----| | `docker exec app_db psql` fails | postgres not running or wrong container name | Check `docker ps` to see running containers, update `app_db` name | | `DROP DATABASE` hangs | connections still open | Run terminate step first before DROP | | SSH auth fails | Wrong key or username | Verify with `aws ec2 describe-instances` | | Old code running after deploy | Image not rebuilt | Run `docker compose build` explicitly | | Port 3001 already in use (local) | Another app using port | `kill $(lsof -t -iTCP:3001 -sTCP:LISTEN)` | --- ## Instance Management (AWS CLI) ```bash # List instances aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" # Start/stop aws ec2 stop-instances --instance-ids $INSTANCE_ID aws ec2 start-instances --instance-ids $INSTANCE_ID # Check instance type aws ec2 describe-instances \ --instance-ids $INSTANCE_ID \ --query "Reservations[0].Instances[0].InstanceType" \ --output text # Check CPU/memory metrics aws cloudwatch get-metric-statistics \ --namespace AWS/EC2 \ --metric-name CPUUtilization \ --dimensions Name=InstanceId,Value=$INSTANCE_ID \ --start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ) \ --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \ --period 300 \ --statistics Average \ --output table ```