Backend Deployment
The aiqlick-backend (NestJS) service deploys to AWS ECS Fargate via
GitHub Actions. After Phase 7 the
SSH-into-EC2 deploy is gone — GitHub Actions now talks to ECS APIs only.
End-to-end deploy time is ~3-5 minutes dominated by the Docker build
(~1-2 min with warm BuildKit cache) and ECS rolling deployment (~2-3 min
including health-check grace period). Schema-change deploys add ~30s for
the one-shot prisma db push task.
Environments
| Environment | ECS service | Task family | Image tag | URL |
|---|---|---|---|---|
| Production | backend-prod (cluster aiqlick) | aiqlick-backend-prod | aiqlick-backend:latest | https://api.aiqlick.com |
| Development | backend-dev (cluster aiqlick) | aiqlick-backend-dev | aiqlick-backend:dev-latest | https://api-dev.aiqlick.com |
Both serve through the shared aiqlick-public-alb with host-header rules
(api.aiqlick.com → tg-backend-prod, api-dev.aiqlick.com → tg-backend-dev).
Deployment Pipeline
Workflow file: .github/workflows/aws-deploy.yml
Runner: runs-on: ubuntu-latest (no self-hosted runner needed).
Branch-to-Environment Mapping
| Branch | Image tag prefix | ECS service | Secret bundle |
|---|---|---|---|
dev | dev- | backend-dev | aiqlick-backend/development |
main | (none) | backend-prod | aiqlick-backend/production |
Docker Build
3-stage multi-stage Dockerfile (unchanged from EC2):
| Property | Value |
|---|---|
| Builder image | node:22-alpine |
| Runner image | node:22-alpine |
| Build deps (builder only) | python3, make, g++ (for bcrypt/sharp native modules) |
| Entrypoint | node dist/src/main.js |
| Healthcheck | wget --spider http://localhost:4001/health (30s interval) |
| Image size | ~240 MB |
| Build time | ~2 minutes (cold), ~30s (warm) |
Schema changes (prisma db push)
When prisma/schema.prisma changes between commits, the workflow runs an
intermediate one-shot Fargate task BEFORE the long-running service rolls.
The task family is aiqlick-prisma-migrate-{env}, image is the new image
just pushed, command is npx prisma db push, network config is copied
from the long-running service so it reaches RDS through the same private
subnets.
If prisma db push fails the workflow aborts — the long-running service
stays on the old image, so the env keeps serving requests with the old
code + old schema. Fix the schema and re-run.
Service deployment
aws-actions/amazon-ecs-deploy-task-definition@v2 registers a new
revision of the service task definition with the new image, calls
UpdateService, and waits for steady state (wait-for-service-stability: true, 10-min cap). The deployment circuit breaker auto-rolls back to the
previous revision if the new tasks fail health checks.
External health verification
After ECS reports steady state, the workflow curls
https://api(.dev).aiqlick.com/health and posts a { __typename } query
to the GraphQL endpoint. Both must return 200.
Manual deployment
If CI is broken and you need to roll a new image manually:
# Resolve current image SHA in ECR
SHA=$(aws ecr describe-images --profile Administrator-842697652860 --region eu-north-1 \
--repository-name aiqlick-backend --query 'imageDetails[0].imageDigest' --output text)
# Force a new deployment of the existing task definition (re-pulls latest tag)
aws ecs update-service --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick --service backend-prod --force-new-deployment
# Wait for stability
aws ecs wait services-stable --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick --services backend-prod
To pin to a specific historical task-def revision:
aws ecs update-service --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick --service backend-prod \
--task-definition aiqlick-backend-prod:42
Troubleshooting
| Problem | Commands |
|---|---|
| Tasks not starting | aws ecs describe-services --cluster aiqlick --services backend-prod --query 'services[0].events[0:10]' |
| Container crash on boot | aws logs tail /ecs/aiqlick-backend-prod --follow --since 10m |
| Health checks failing | aws elbv2 describe-target-health --target-group-arn $(aws elbv2 describe-target-groups --names tg-backend-prod --query 'TargetGroups[0].TargetGroupArn' --output text) |
| Bad secret bundle | aws ecs execute-command --cluster aiqlick --task ... --container aiqlick-backend --interactive --command 'sh' && env | grep -i secret |
Related
- ECS Fargate — cluster + service inventory
- EC2 → ECS migration runbook
- AI Service Deployment — sister service, same pattern
- CI/CD Pipelines