EC2 Deploy Skill

SkillDatabases & data

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the EC2 Deploy Skill skill

What this skill tells your AI

The instructions your AI receives, as published by adam-s/intercept in .claude/skills/ec2-deploy/SKILL.md and read by ahel’s review.

⚠️💣 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.

VariableDescriptionExample
INSTANCE_NAMEEC2 instance name tagmy-app-prod
SSH_KEYLocal SSH private key~/.ssh/my-app-prod-key.pem
SSH_USEROS user on instanceubuntu (or ec2-user)
PROJECT_DIRGit repo root on instance/home/ubuntu/my-project
COMPOSE_FILEDocker Compose configdocker-compose.prod.yml
DB_USERDatabase usernameappuser
DB_NAMEDatabase nameapp_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:

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

ssh -i $SSH_KEY -o StrictHostKeyChecking=no $SSH_USER@<PUBLIC_IP>
  • 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:

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:

ContainerImagePurpose
app_dbtimescale/timescaledb or postgresPostgreSQL database
app_redisredis:7-alpineJob queue (BullMQ)
app_apiyour-app-api (local build)API server
app_webyour-app-web (local build)Frontend (Next.js or similar)
app_proxycaddy:2-alpineReverse 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):

ssh -i $SSH_KEY $SSH_USER@<IP> "
  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

git push origin main

Step 2: Pull on EC2

ssh -i $SSH_KEY $SSH_USER@<IP> "
  cd $PROJECT_DIR && git pull origin main && git log --oneline -3
"

Step 3: Drop and recreate the database

ssh -i $SSH_KEY $SSH_USER@<IP> "
  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)

ssh -i $SSH_KEY $SSH_USER@<IP> "
  cd $PROJECT_DIR
  docker compose -f $COMPOSE_FILE stop api web
"

Step 5: Rebuild images

ssh -i $SSH_KEY $SSH_USER@<IP> "
  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

ssh -i $SSH_KEY $SSH_USER@<IP> "
  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

ssh -i $SSH_KEY $SSH_USER@<IP> "
  cd $PROJECT_DIR
  docker compose -f $COMPOSE_FILE up -d
"

Step 8: Verify

# Container health
ssh -i $SSH_KEY $SSH_USER@<IP> \
  "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@<IP> "
  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

ssh -i $SSH_KEY $SSH_USER@<IP> \
  "docker exec app_db psql -U $DB_USER -d $DB_NAME -c 'SELECT COUNT(*) FROM users;'"

View container logs

ssh -i $SSH_KEY $SSH_USER@<IP> \
  "docker logs app_api --tail 50 -f"

Run a script in the api container

ssh -i $SSH_KEY $SSH_USER@<IP> "
  cd $PROJECT_DIR
  docker compose -f $COMPOSE_FILE run --rm --no-deps api \
    sh -c 'pnpm run scripts/my-script.ts'
"

Debugging Common Issues

IssueLikely CauseFix
docker exec app_db psql failspostgres not running or wrong container nameCheck docker ps to see running containers, update app_db name
DROP DATABASE hangsconnections still openRun terminate step first before DROP
SSH auth failsWrong key or usernameVerify with aws ec2 describe-instances
Old code running after deployImage not rebuiltRun docker compose build explicitly
Port 3001 already in use (local)Another app using portkill $(lsof -t -iTCP:3001 -sTCP:LISTEN)

Instance Management (AWS CLI)

# 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

Signals

GitHub stars
187
Forks
18
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
ec2-deploy
Source
github.com/adam-s/intercept