Ini artikel pertama gua di dev.to. Gua nulis ini karena makanan sehari-hari deploy NestJS ke ECS Fargate dan sebagai catetan gua juga di kala lupa.
Gua bakal nulis dari pengalaman nyata setup yang gua pake di kerjaan, bukan setup ideal tapi work in production karena waktu dan tenaga yang terbatas.
konten bahasa Indonesia yang ngebahas ini step by step jarang kayaknya ya wkwk atau terlalu niche ya? ajak-ajak gua lah kalo ada pembahasan terkait AWS khususnya DevOps dan WebDev.
buat step by step gua bikin masih narrative tapi gua coba bahas sedetail mungkin, gambarnya bertahap gua lengkapin
kemungkinan nantinya tulisan ini bakal ngelink ke beberapa tutorial lainnya dan aka di update bertahap.
Semoga artikel ini ngebantu lo yang lagi di posisi yang sama.
Kenapa ECS Fargate, Bukan EC2?
Kalau lo baru mau mulai deploy NestJS app ke AWS, pilihan pertama yang sering bikin bingung adalah: pakai EC2 atau ECS?
EC2 itu simpel secara konsep, lo dapat satu virtual machine (temen gua beberapa ada yang kecele dia rada binggung saat gua bilang VM, gua bilang kayak semacam VPS baru nyambung), lo install Node.js, lo jalanin app lo. Tapi di balik itu lo harus urus sendiri: update OS, manage process dengan PM2, handle restart kalau app crash, dan kalau mau scale lo harus setup sendiri.
ECS Fargate beda. Lo ga pegang server sama sekali. Lo cukup kasih container, define berapa CPU dan memory yang dibutuhkan, dan AWS yang urus sisanya kayak scheduling, restart, bahkan scaling kalau lo mau. Lo bayar per detik container jalan, bukan per jam instance hidup.
Artikel ini bakal ngajarin lo cara deploy NestJS app ke ECS Fargate secara manual tanpa CI/CD dulu. Cukup dari local, push ke ECR, setup di console AWS, dan app lo jalan di belakang ALB.
Ini cara yang paling gampang buat mulai, dan kalau lo udah paham flow-nya, nambah CI/CD belakangan jadi jauh lebih gampang.
Prasyarat
Sebelum mulai, pastiin lo udah punya hal-hal berikut:
AWS Side:
- AWS account yang udah aktif
- IAM user dengan akses ke ECR dan ECS (jangan pakai root account)
- ECR repository yang udah dibuat
- RDS PostgreSQL instance yang udah jalan di VPC yang sama
- ALB yang udah di-setup atau siap di-setup
Local Side:
- AWS CLI v2 terinstall dan sudah di-configure (
aws configure) - Docker Desktop terinstall dan jalan
- NestJS app yang udah siap di-deploy
- Git (karena script deploy kita bakal pakai branch name dan commit hash buat image tag)
Networking:
- VPC dengan minimal 2 public subnet untuk ALB
- Security group yang udah direncanain ECS task perlu bisa terima traffic dari ALB, dan bisa konek ke RDS
Kalau semua udah siap, kita mulai dari langkah pertama.
Step 1: Dockerize NestJS App
Buat Dockerfile di root project lo:
# ---------- builder stage ----------
FROM node:22-alpine AS builder
WORKDIR /usr/app
# install build deps (include dev deps because we need tsc)
COPY package*.json ./
RUN npm ci
# copy source & build
COPY . .
RUN npm run build
# ---------- production stage ----------
FROM node:22-alpine
WORKDIR /usr/app
# copy only production deps and app metadata
COPY package*.json ./
# install only production deps
RUN npm ci --omit=dev
# copy built artifacts from builder
COPY --from=builder /usr/app/dist ./dist
# create non-root user (optional but recommended)
RUN addgroup -S app && adduser -S app -G app
# create exports directory with proper permissions
RUN mkdir -p /usr/app/exports && chown -R app:app /usr/app
USER app
# GANTI INI -- sesuaikan dengan port NestJS app lo
EXPOSE 3000
CMD ["node", "dist/main.js"]
Beberapa hal yang perlu diperhatiin:
- Pakai multi-stage build. Stage pertama buat build, stage kedua buat production image. Hasilnya image lebih kecil karena ga bawa devDependencies.
-
node:22-alpineversi LTS terbaru, lebih ringan dari image full. -
npm ci --omit=devlebih modern dari--only=production, install hanya production dependencies. - Non-root user ini security best practice yang sering dilewatin. Container lo ga jalan sebagai root, jadi kalau ada exploit, dampaknya lebih terbatas.
-
mkdir exportskalau app lo ada fitur generate file, direktori ini udah siap dengan permission yang bener.
Test dulu di local sebelum push ke ECR:
docker build -t nama-app-lo .
docker run -p 3000:3000 nama-app-lo # GANTI INI - sesuaikan port
Step 2: Setup ECR dan Push Image
Buat ECR Repository
Masuk ke AWS Console > ECR > Create repository. Kasih nama yang deskriptif, misalnya nama-project/nama-service.
kalo mau liat push commandnya tinggal klik repository yang udah dibuat terus klik disana ada button "View Push Command"
tapi gua ada cara lebih gampang lagi biar ga berkali-kali push command kalo mau push image
Script Deploy ke ECR
Ini script yang gua pake buat build dan push image dari local ke ECR:
#!/usr/bin/env bash
set -e
# Pastiin AWS CLI ada di PATH lo
export PATH="/c/Program Files/Amazon/AWSCLIV2:$PATH"
echo "Using AWS CLI from: $(which aws.exe)"
# Variables
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
IMAGE_TAG="${BRANCH_NAME}-$(git rev-parse --short HEAD)"
LAST_COMMIT=$(git log -1 --pretty=format:"%h - %s (%cr) <%an>")
REGION="ap-southeast-1" # GANTI INI - sesuaikan region lo
ECR_URI="YOUR_ACCOUNT_ID.dkr.ecr.${REGION}.amazonaws.com/YOUR_REPO_NAME" # GANTI INI
# Display deployment info
echo "=== Deployment Information ==="
echo "Branch: ${BRANCH_NAME}"
echo "Commit: ${LAST_COMMIT}"
echo "Image Tag: ${IMAGE_TAG}"
echo "================================"
# Authenticate Docker ke ECR
aws.exe ecr get-login-password --region "${REGION}" \
| docker login --username AWS --password-stdin "${ECR_URI}"
# Build & tag
docker build -t nama-app-lo . # GANTI INI
docker tag nama-app-lo:latest "${ECR_URI}:latest" # GANTI INI
docker tag nama-app-lo:latest "${ECR_URI}:${IMAGE_TAG}" # GANTI INI
# Push
docker push "${ECR_URI}:latest"
docker push "${ECR_URI}:${IMAGE_TAG}"
echo "Deployed image tag: ${IMAGE_TAG}"
Kenapa gua push dua tag sekaligus?
-
latestyang direferensiin di ECS task definition, jadi tiap deploy ECS tinggal pull yang terbaru -
branch-commithashbuat versioning, kalau ada masalah lo tau persis commit mana yang jalan di production - ini kepake banget saat lo nantinya di minta buat pisahan enviroment misalnya development, staging dan production lo ga binggung enviroment mana pake image yang mana dan branch. karena real casenya dev, stag, prod itu beda-beda semua enviromentnya saat ada keterbatasan team ini bisa nolong lo dari kepusingan itu
Step 3: Simpan .env di S3
Ini bagian yang jarang dibahas tapi sangat praktis. Daripada hardcode environment variable satu-satu di task definition, lo bisa simpan .env file di S3 dan ECS yang bakal load otomatis waktu container start.
Kenapa S3?
- Lebih gampang di-manage, tinggal update file di S3, ga perlu update task definition
- Lebih aman, file ada di S3 dengan permission yang bisa dikontrol
- Familiar, format
.envyang sama kayak di local
Cara Setup
1. Buat S3 bucket khusus buat env files:
nama-bucket-lo/
nama-app-lo/
staging/
.env
production/
.env
2. Upload .env ke S3:
aws s3 cp .env s3://nama-bucket-lo/nama-app-lo/staging/.env # GANTI INI
3. Kasih permission ke ECS Task Execution Role:
ECS butuh permission buat baca file dari S3. Tambahkan policy ini ke ecsTaskExecutionRole di IAM:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::nama-bucket-lo/*"
},
{
"Effect": "Allow",
"Action": ["s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::nama-bucket-lo"
}
]
}
Step 4: Setup Task Definition
Task definition itu blueprint container lo di ECS. Define image mana yang dipakai, berapa resource yang dialokasiin, port mana yang dibuka, dan dari mana env-nya diload.
Ini contoh task definition yang gua pake:
{
"family": "nama-service-lo",
"containerDefinitions": [
{
"name": "nama-service-lo",
"image": "YOUR_ACCOUNT_ID.dkr.ecr.ap-southeast-1.amazonaws.com/YOUR_REPO:latest",
"cpu": 0,
"portMappings": [
{
"containerPort": 3000,
"hostPort": 3000,
"protocol": "tcp"
}
],
"essential": true,
"environment": [],
"environmentFiles": [
{
"value": "arn:aws:s3:::nama-bucket-lo/nama-app-lo/staging/.env",
"type": "s3"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/nama-service-lo",
"awslogs-region": "ap-southeast-1",
"awslogs-stream-prefix": "ecs"
}
}
}
],
"executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512"
}
Beberapa hal yang perlu diperhatiin:
-
cpu: 256danmemory: 512itu spec paling minimal di Fargate, dan perlu diingat ini dialokasiin di level task, bukan per container. Artinya kalau lo punya lebih dari satu container di task yang sama, semua sharing pool ini. Untuk single container staging cukup, tapi kalau lo mau nambah container lain di task yang sama, naikin spec dulu sebelum deploy. -
networkMode: awsvpcmandatory buat Fargate, setiap task dapat ENI sendiri. -
logDriver: awslogslog container lo otomatis masuk ke CloudWatch, sangat membantu waktu debugging. -
environmentFilesyang nge-load.envdari S3 yang kita setup di step sebelumnya.
Buat CloudWatch log group dulu sebelum deploy. Ada dua cara:
Via AWS CLI:
# Buat log group
aws logs create-log-group \
--log-group-name /ecs/nama-service-lo \
--region ap-southeast-1 # GANTI INI
# Set retention 30 hari supaya ga numpuk dan kena billing
aws logs put-retention-policy \
--log-group-name /ecs/nama-service-lo \
--retention-in-days 30 \
--region ap-southeast-1 # GANTI INI
Via AWS Console:
Masuk ke CloudWatch > Log groups > Create log group. Isi log group name dengan /ecs/nama-service-lo, lalu di bagian Retention setting pilih 30 days. Kalau lo skip retention policy, log lo bakal disimpan selamanya dan kena billing terus.
Buat Cluster
Masuk ke AWS Console > ECS > Clusters > Create Cluster. Pilih AWS Fargate sebagai infrastructure.
Step 6: Setup ALB dan Target Group
Sebelum buat service, lo harus siapin ALB dan target group dulu karena lo bakal attach ini waktu buat service di Step selanjutnya.
Buat Target Group
Masuk ke EC2 > Target Groups > Create:
- Target type: IP (bukan Instance, karena Fargate pakai awsvpc mode)
- Protocol: HTTP
- Port: sesuaikan dengan port container lo
-
Health check path:
/atau endpoint health check lo kalau ada (biasanya gua pake pattern/api/v1/health) isinya json biasa aja
Buat ALB
Masuk ke EC2 > Load Balancers > Create > Application Load Balancer:
- Scheme: Internet-facing (kalau mau diakses dari luar)
- Subnets: minimal 2 AZ, pilih public subnet
- Security group: allow inbound port 80 dan/atau 443
- Listener: forward ke target group yang baru lo buat
Setelah ALB dan target group siap, baru lo buat service ECS-nya.
Buat Service
Masuk ke cluster yang baru lo buat, klik Create service:
- Isi konfigurasi dasar:
- Launch type: Fargate
- Task definition: pilih yang lo buat di Step 4
- Service name: kasih nama yang deskriptif
- Desired tasks: 1 untuk awal
- Di bagian Networking:
- Pilih VPC yang sama dengan RDS lo
- Pilih minimal 2 subnet
- Buat atau pilih security group yang allow inbound dari ALB dan allow outbound ke RDS
- Di bagian Load balancing:
- Pilih Application Load Balancer
- Pilih ALB yang baru lo buat
- Pilih target group yang sesuai
- Pastiin health check path lo bener
- Klik Create service
ECS bakal langsung pull image dari ECR dan jalanin task pertama lo. Setelah task RUNNING, ECS otomatis register ke target group dan ALB mulai routing traffic ke container lo.
Step 7: Deploy Ulang Setelah Setup Awal
Step 5 dan 6 itu cuma dilakuin sekali waktu pertama kali setup. Setelah infrastruktur jalan, workflow deploy lo jadi jauh lebih simpel.
Tiap kali ada update code, lo cukup:
1. Push image baru ke ECR:
./deploy.sh
2. Trigger ECS buat pull image terbaru:
#!/usr/bin/env bash
set -e
REGION="ap-southeast-1" # GANTI INI
CLUSTER="nama-cluster-lo" # GANTI INI
SERVICE="nama-service-lo" # GANTI INI
aws.exe ecs update-service \
--region "${REGION}" \
--cluster "${CLUSTER}" \
--service "${SERVICE}" \
--force-new-deployment > /dev/null 2>&1
echo "✅ Service '${SERVICE}' updated"
--force-new-deployment yang bikin ECS stop task lama dan start task baru dengan image latest yang baru aja lo push. Script ini cuma bisa dijalanin kalau service lo udah exist, makanya setup awal di Step 5 harus selesai dulu.
Dua script, selesai.
Step 8: Verifikasi
Setelah update-service jalan, tunggu beberapa menit (biasanya 2-5 menit) dan cek:
-
ECS Console > cluster lo > service > Tasks, pastiin task statusnya
RUNNING -
CloudWatch Logs, cek
/ecs/nama-service-lobuat liat log container lo -
Target Group, cek health check statusnya
healthy - ALB DNS, akses DNS ALB lo di browser, harusnya app lo udah jalan
Kalau task gagal start, logs di task atau logs CloudWatch itu tempat pertama yang harus lo cek. keterangan log nya itu sama persis ketika lo jalanin di lokal seperti log error, warning, info dll
Lessons Learned
Beberapa hal yang gua pelajarin dari setup ini di production:
1. Health check itu krusial
Kalau health check path lo salah atau endpoint lo return status bukan 200, ALB bakal terus-terusan drain dan replace task dan ini yang pertama kali gua pikir selalu gagal 2 hari debugging ini ternyata dia nyari yang response 200. Pastiin health check path lo bener sebelum deploy.
2. Security group harus teliti
ECS task, ALB, dan RDS punya security group masing-masing. Yang paling sering bikin masalah: ECS task ga bisa konek ke RDS karena security group RDS belum allow inbound dari security group ECS. Cek ini kalau app lo gagal konek ke database.
3. S3 env file ga otomatis refresh
ini juga pain point, Kalau lo update .env di S3, container yang lagi jalan ga otomatis reload. Lo tetap harus trigger --force-new-deployment buat apply perubahan env.
4. Minimal spec dulu
Mulai dari 256 CPU / 512 MB memory. Monitor dulu di CloudWatch, baru scale up kalau emang butuh. Jangan langsung pakai spec besar karena Fargate billing per resource yang lo define.
Penutup
Setup ini mungkin keliatan banyak langkahnya, tapi setelah semua infrastruktur jalan, workflow deploy harian lo cuma dua script. Simpel dan bisa dikontrol.
Dari sini, langkah selanjutnya yang natural adalah:
- Tambah CI/CD supaya ga perlu build dari local (AWS CodeBuild)
- Setup auto scaling di ECS service
- Pisahin staging dan production environment
Kalau ada pertanyaan atau ada yang kurang jelas, drop di kolom komentar. Gua bakal jawab sesuai pengalaman gua.
Top comments (0)