Production Pipeline
এই page-এ একটি সম্পূর্ণ production pipeline লিখবেন — Next.js frontend, Express API, PostgreSQL database এবং Prisma migration একসাথে। Deploy fail করলে pipeline নিজে থেকে আগের image-এ ফিরে যাবে।
Before You Start
সরাসরি এই page-এ এলে নিচের জিনিসগুলো তৈরি থাকতে হবে:
| যা লাগবে | কোথায় পাবেন |
|---|---|
Server-এ Docker, /opt/myapp folder | Setup |
| GitHub-এ তিনটি secret | Setup |
| Registry-তে access — package public, নয়তো server-এ login | Setup |
| একটি কাজ করা basic pipeline | First Pipeline |
Basic pipeline না বুঝে থাকলে আগে ওই page-টি দেখে নিন। এখানে সম্পূর্ণ file দেওয়া আছে ঠিকই, কিন্তু TAG কীভাবে কাজ করে, needs বা concurrency কেন লাগে — তার ব্যাখ্যা ওখানে আছে, এখানে আবার লেখা হয়নি।
What Changes
আগের pipeline একটি image deploy করত। এখন তিনটি নতুন জিনিস যোগ হলো।
| First Pipeline | Production Pipeline | |
|---|---|---|
| Image সংখ্যা | ১টি — myapp | ২টি — myapp-web, myapp-api |
| Database | নেই | PostgreSQL container |
| Schema পরিবর্তন | নেই | Prisma migration |
| Deploy-এর আগে | কিছু না | database backup |
Image দুটি হয়ে গেল, তাই নামেও -web আর -api যোগ করা হলো। একটির বেশি image থাকলে নাম দেখেই বোঝা দরকার কোনটা কী।
আগের page-টি শেষ করে এখানে এসে থাকলে আপনার server-এ ইতিমধ্যে একটি চলতি setup আছে — myapp নামের একটি package আর একটি কাজ করা compose.yaml। নাম বদলে যাচ্ছে, তাই তিনটি কাজ করতে হবে:
- Server-এর
compose.yamlপুরোটা বদলে ফেলুন নিচের নতুন version দিয়ে। পুরোনোwebservice-এর image-এর নামও বদলাবে —myappথেকেmyapp-web .env-এ নতুন লাইনগুলো যোগ করুন —POSTGRES_*আরDATABASE_URL।TAGলাইনটি যেমন আছে তেমনই থাকুক- পুরোনো
myapppackage-টি এখনই মুছবেন না। নতুন pipeline কাজ করা শুরু করলে তারপর GitHub-এ গিয়ে মুছবেন। ততক্ষণ ওটাই আপনার ফেরার পথ
আরেকটি কথা জেনে রাখুন — প্রথম deploy-এ db container একেবারে নতুন করে তৈরি হবে, তাই database খালি থাকবে। আগের কোনো data থাকলে সেটি আলাদাভাবে import করে নিতে হবে।
সবচেয়ে কঠিন অংশটি database। Image rollback করা সহজ — পুরোনো tag বসিয়ে দিলেই হয়। কিন্তু একবার migration চলে গেলে database-এর গঠন বদলে গেছে, সেটি ফেরানো সহজ নয়। তাই এই page-এর বড় অংশ migration নিয়ে।
Repository Layout
দুটি app একই repository-তে রাখব। একে monorepo বলে। একটি push, একটি pipeline, দুটি image।
- ci.yml
- deploy.yml
- Dockerfile
- .dockerignore
- tsconfig.json
- package.json
- package-lock.json
দুটি folder, দুটি Dockerfile, দুটি image। Server-এ এখনো সেই দুটি file-ই থাকবে — compose.yaml আর .env।
Repository-র root-এ কোনো file নেই — সব কিছু app folder-এর ভেতরে। এর কারণ Docker।
নিচের workflow-এ প্রতিটি image-এর build context হলো ./apps/web বা ./apps/api। Docker ওই folder-এর বাইরের কিছুই দেখতে পায় না। তাই দুটি জিনিস প্রতিটি app-এর ভেতরে থাকতে হবে:
- নিজের
package-lock.json— root-এ রাখলেnpm ciসেটি খুঁজে পাবে না, build fail করবে - নিজের
.dockerignore— root-এ রাখলে Docker সেটি পড়বেই না, আর আপনার local.envimage-এর ভেতরে ঢুকে registry-তে চলে যাবে
দুটি app-এর .dockerignore-এ একই content:
node_modules
.next
dist
.git
.env
.env.*
Dockerfile
.dockerignorenpm workspaces ব্যবহার করলে lock file একটিই হয়, root-এ। তখন build context root করতে হয় আর Dockerfile-এর প্রতিটি path বদলাতে হয়। এই guide-এ সেই জটিলতায় যাচ্ছি না।
দুটি আলাদা repository রাখলেও চলে। তখন দুটি আলাদা deploy.yml লিখতে হবে, এবং API আগে না পরে deploy হবে সেটি নিজে সামলাতে হবে। ছোট ও মাঝারি project-এ monorepo অনেক কম ঝামেলার।
The API Dockerfile
Web-এর Dockerfile আগের page-এ আছে। API-রটি আলাদা, কারণ এতে Prisma আছে।
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx prisma generate
RUN npm run build
# dev dependency গুলো ফেলে দিই, image ছোট হবে
RUN npm prune --omit=dev
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
USER node
EXPOSE 4000
CMD ["node", "dist/server.js"]তিনটি জিনিস খেয়াল করুন।
package.json final image-এ যাচ্ছে
Node এই file দেখে বোঝে code-টি ESM নাকি CommonJS। এটি না থাকলে "type": "module" ব্যবহার করা project-এ Cannot use import statement outside a module error আসবে। Prisma-ও এই file পড়ে।
npx prisma generate build-এর আগে
Prisma আপনার schema.prisma পড়ে একটি client তৈরি করে। এটি না বানালে TypeScript build fail করবে, কারণ prisma.user.findMany() জাতীয় জিনিস তখনো তৈরি হয়নি।
prisma folder final image-এ যাচ্ছে
Deploy-এর সময় container-এর ভেতরে migration চালাতে হবে। তার জন্য migration file গুলো image-এর ভেতরে থাকা দরকার।
prisma package-টি package.json-এ devDependencies-এ থাকলে সেটিকে dependencies-এ সরান।
"dependencies": {
"@prisma/client": "^6.0.0",
"prisma": "^6.0.0"
}কারণ npm prune --omit=dev লাইনটি সব dev dependency মুছে দেয়। prisma মুছে গেলে container-এর ভেতরে prisma migrate deploy চালানো যাবে না।
The Server Files
Server-এ এখনো মাত্র দুটি file। শুধু ভেতরে আরো কিছু যোগ হলো।
services:
web:
image: ghcr.io/your-username/myapp-web:${TAG}
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
NODE_ENV: production
depends_on:
- api
api:
image: ghcr.io/your-username/myapp-api:${TAG}
restart: unless-stopped
ports:
- "127.0.0.1:4000:4000"
env_file: .env
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:চারটি জিনিস খেয়াল করুন।
condition: service_healthy
সাধারণ depends_on শুধু container চালু হওয়া পর্যন্ত অপেক্ষা করে। কিন্তু PostgreSQL container চালু হওয়ার পরেও কয়েক সেকেন্ড connection নিতে পারে না। এই লাইনটি API-কে অপেক্ষা করায় যতক্ষণ না database সত্যিই প্রস্তুত।
db-data volume
Container মুছে গেলেও data থাকবে। এই লাইনটি না থাকলে deploy-এর সময় container বদলানোর সাথে সাথে আপনার পুরো database মুছে যাবে।
Database-এর কোনো port বাইরে খোলা নেই
web আর api-তে ports আছে, db-তে নেই। তবু API database-এ পৌঁছাতে পারে, কারণ একই compose network-এর ভেতরে container-রা একে অপরকে নামে চেনে।
web-এ env_file নেই, শুধু api-তে আছে
.env file-এ database-এর password আছে। Frontend-এর সেটি জানার কোনো দরকার নেই, তাই তাকে দেওয়াও হয়নি। কোনো container-এ কেউ ঢুকে পড়লে সে যেন সবকিছু না পায় — এটিই উদ্দেশ্য।
তবে সৎভাবে বলি — env_file: .env লেখার মানে হলো api ওই file-এর সব value পাচ্ছে। POSTGRES_PASSWORD আর TAG-ও যাচ্ছে, যদিও api-র শুধু DATABASE_URL দরকার।
আরো শক্ত করতে চাইলে env_file বাদ দিয়ে নাম ধরে ধরে দিন:
api:
environment:
DATABASE_URL: ${DATABASE_URL}Compose তখনো .env পড়বে ${DATABASE_URL} বসানোর জন্য, কিন্তু container-এর ভেতরে শুধু ওই একটি variable-ই যাবে।
এই guide-এ env_file রাখা হয়েছে কারণ app বড় হলে variable-এর সংখ্যা বাড়তে থাকে, আর তখন প্রতিটি নাম হাতে লিখে রাখা ক্লান্তিকর হয়ে যায়। কোনটা বেছে নেবেন সেটি আপনার সিদ্ধান্ত।
এবার .env:
TAG=latest
POSTGRES_USER=myapp
POSTGRES_PASSWORD=a-long-random-password
POSTGRES_DB=myapp
DATABASE_URL=postgresql://myapp:a-long-random-password@db:5432/myappDATABASE_URL-এ localhost নয়, db লেখা আছে। এটি compose file-এ দেওয়া service-এর নাম।
Container-এর ভেতর থেকে localhost মানে ওই container নিজে — database নয়। এই এক জায়গায় প্রায় সবাই একবার আটকায়।
এই file-এ database-এর password আছে। Server-এ গিয়ে permission শক্ত করে দিন, যাতে শুধু আপনার user পড়তে পারে:
chmod 600 /opt/myapp/.envDocker section-এ Compose Secrets নামে একটি পদ্ধতি দেখেছেন, যেখানে password আলাদা file-এ থাকে আর container-এর ভেতরে /run/secrets/ হয়ে আসে। সেটি .env-এর চেয়ে শক্ত, কারণ তখন password কোনো environment variable-এ বসে না — docker inspect করলেও চোখে পড়ে না।
এখানে .env ব্যবহার করা হয়েছে কারণ Prisma-র DATABASE_URL একটি সম্পূর্ণ connection string চায়। Secret file দিয়ে সেটি বানাতে গেলে app-এর code-এ পরিবর্তন আনতে হয়, আর docker compose run দিয়ে migration চালানোও জটিল হয়ে যায়।
একটি server-এ চলা project-এ chmod 600 করা .env যথেষ্ট নিরাপদ। একাধিক মানুষের server-এ access থাকলে তখন Compose Secrets-এ যাওয়ার কথা ভাববেন।
Build-Time Variables
NEXT_PUBLIC_ দিয়ে শুরু হওয়া কোনো variable এই file-এ রাখবেন না। রাখলেও কাজ হবে না।
Next.js এই variable গুলো build-এর সময় JavaScript bundle-এর ভেতরে বসিয়ে দেয়। অর্থাৎ image বানানোর মুহূর্তেই value স্থায়ীভাবে ঢুকে যায়। Server-এর .env-এ পরে লিখলে আর কিছু বদলায় না।
এগুলো দিতে হয় build-এর সময়। Workflow-এ:
- name: Build and push web
uses: docker/build-push-action@v6
with:
context: ./apps/web
build-args: |
NEXT_PUBLIC_API_URL=https://yourdomain.com/apiআর apps/web/Dockerfile-এর builder stage-এ সেটি গ্রহণ করতে হয়:
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run buildএই কারণেই NEXT_PUBLIC_-এ কখনো secret দেবেন না। Value-টি image-এর ভেতরে বসে যায় এবং browser-এ চলে যায় — যে কেউ page source খুলে দেখতে পারে।
DATABASE_URL আর POSTGRES_* build-এর সময় লাগে না। ওগুলো container চালু হওয়ার সময় পড়া হয়, তাই .env-এই ঠিক আছে।
Migration Order
এটি এই page-এর সবচেয়ে গুরুত্বপূর্ণ অংশ। ভুল order মানে site down।
migrate deploy vs migrate dev
Prisma-র দুটি migration command আছে। Pipeline-এ শুধু একটি চলতে পারে — migrate deploy।
prisma migrate dev | prisma migrate deploy | |
|---|---|---|
| কাজ | নতুন migration file তৈরি করে | শুধু তৈরি থাকা migration চালায় |
| Database reset | করতে পারে | কখনো না |
| প্রশ্ন করে | হ্যাঁ | না |
| কোথায় | আপনার laptop-এ | Pipeline-এ |
Pipeline-এ কখনো prisma migrate dev লিখবেন না। Schema-তে এমন কিছু পেলে যা migration history-র সাথে মেলে না, এই command production database drop করে নতুন করে বানিয়ে ফেলতে পারে। আপনার সব data মুহূর্তে শেষ।
Migration file আপনার laptop-এ তৈরি হবে, Git-এ commit হবে, আর server-এ শুধু migrate deploy দিয়ে চালানো হবে।
আপনার নিজের computer-এ কাজের নিয়ম:
cd apps/api
npx prisma migrate dev --name add_user_phone
git add prisma/migrations
git commit -m "add phone column to user"The Correct Order
Server-এ command গুলো এই ক্রমে চলবে:
১. .env-এ নতুন TAG image ঠিক করা
২. docker compose pull নতুন image নামানো (এখনো চালু নয়)
৩. database backup নিরাপত্তা জাল
৪. prisma migrate deploy schema বদলানো
৫. docker compose up -d নতুন container চালু
৬. health check সত্যিই চলছে কিনাকেন এই order, দুটি কারণ জেনে রাখুন।
Build এখানে নেই কেন?
Build ইতিমধ্যে runner-এ হয়ে গেছে, আর needs: build থাকায় সেটি পাশ না করলে এই script চালুই হতো না। অর্থাৎ database ছোঁয়ার আগেই নিশ্চিত হয়ে গেছে যে code build হয়।
এটি server-এ build করার চেয়ে ভালো। ওখানে build fail করলে আপনি অর্ধেক পথে আটকে যেতেন।
Migration কেন up -d-র ঠিক আগে?
Migration আর নতুন container চালু হওয়ার মাঝের সময়টুকুতে নতুন database গঠন, পুরোনো code — এই অবস্থা থাকে। এই সময় যত কম হয় তত ভালো। তাই দুটি পাশাপাশি রাখা হয়েছে।
Safe Migrations
উপরের ওই ছোট সময়টুকুর কারণেই একটি নিয়ম মানতে হয়: migration এমন হতে হবে যা পুরোনো code চালু থাকা অবস্থাতেও ভাঙে না।
| কাজ | নিরাপদ? | কীভাবে করবেন |
|---|---|---|
| নতুন column যোগ (nullable) | হ্যাঁ | সরাসরি করুন |
নতুন column যোগ (NOT NULL + default) | হ্যাঁ | সরাসরি করুন |
নতুন column যোগ (NOT NULL, default নেই) | না | আগে nullable দিন, data ভরুন, পরে NOT NULL |
| নতুন table | হ্যাঁ | সরাসরি করুন |
| Column-এর নাম বদল | না | তিন deploy লাগবে |
| Column মুছে ফেলা | না | দুই deploy লাগবে |
Column-এর নাম বদলানো সবচেয়ে বিপজ্জনক। name কে full_name করতে চাইলে একবারে করবেন না — পুরোনো code তখনো name খুঁজবে এবং crash করবে।
সঠিক পদ্ধতি:
Deploy 1: full_name column যোগ করুন। name-ও থাকুক।
Code দুটোতেই লিখবে, পড়বে name থেকে।
Deploy 2: পুরোনো row গুলোতে name থেকে full_name-এ data copy করুন।
Code এখন full_name থেকে পড়বে।
Deploy 3: name column drop করুন।ধীর মনে হচ্ছে। কিন্তু প্রতিটি deploy-এ site একটুও down হয় না, আর যেকোনো ধাপে থেমে যাওয়া যায়।
Update the CI File
আগের page-এর ci.yml এখানে আর কাজ করবে না। সেটি repository-র root-এ একটি Dockerfile খুঁজত, কিন্তু এখন root-এ কোনো Dockerfile নেই — দুটি আছে, দুই folder-এ।
name: CI
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
app: [web, api]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build ${{ matrix.app }}
uses: docker/build-push-action@v6
with:
context: ./apps/${{ matrix.app }}
push: false
cache-from: type=gha,scope=${{ matrix.app }}
cache-to: type=gha,mode=max,scope=${{ matrix.app }}strategy.matrix একটি সুবিধাজনক জিনিস। এটি একই job-কে দুইবার চালায় — একবার app = web নিয়ে, আরেকবার api নিয়ে। দুটি একসাথে সমান্তরালে চলে, তাই সময়ও বাড়ে না।
scope আলাদা রাখা হয়েছে যাতে দুটি app-এর Docker cache মিশে না যায়।
The Complete Workflow File
এবার পুরো file। আগের সব আলোচনা এখানে একসাথে।
name: Deploy
on:
push:
branches: [main]
concurrency:
group: production-deploy
cancel-in-progress: false
env:
IMAGE_WEB: ghcr.io/your-username/myapp-web
IMAGE_API: ghcr.io/your-username/myapp-api
jobs:
# Job 1: দুটি image বানিয়ে registry-তে পাঠানো
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push web
uses: docker/build-push-action@v6
with:
context: ./apps/web
push: true
tags: |
${{ env.IMAGE_WEB }}:${{ github.sha }}
${{ env.IMAGE_WEB }}:latest
cache-from: type=gha,scope=web
cache-to: type=gha,mode=max,scope=web
- name: Build and push api
uses: docker/build-push-action@v6
with:
context: ./apps/api
push: true
tags: |
${{ env.IMAGE_API }}:${{ github.sha }}
${{ env.IMAGE_API }}:latest
cache-from: type=gha,scope=api
cache-to: type=gha,mode=max,scope=api
# Job 2: দুটি image তৈরি হলে তবেই server-এ deploy
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
command_timeout: 20m
script: |
set -eE
cd /opt/myapp
# ফেরার ঠিকানা মনে রাখি
OLD_TAG=$(grep '^TAG=' .env | cut -d= -f2)
NEW_TAG=${{ github.sha }}
echo "old: $OLD_TAG"
echo "new: $NEW_TAG"
# যেকোনো ধাপে fail করলে এই function চলবে
rollback() {
if [ "$OLD_TAG" = "latest" ] || [ -z "$OLD_TAG" ]; then
echo "previous tag is not a fixed version — cannot roll back"
return
fi
echo "rolling back to $OLD_TAG"
sed -i "s|^TAG=.*|TAG=$OLD_TAG|" .env
docker compose up -d || true
}
trap rollback ERR
# ১. নতুন version বসিয়ে image নামাই (চালু করি না)
sed -i "s|^TAG=.*|TAG=$NEW_TAG|" .env
docker compose pull
# .env থেকে শুধু যে দুটি value লাগবে সেগুলো পড়ি
POSTGRES_USER=$(grep '^POSTGRES_USER=' .env | cut -d= -f2-)
POSTGRES_DB=$(grep '^POSTGRES_DB=' .env | cut -d= -f2-)
# ২. database চালু আছে কিনা নিশ্চিত করি, তারপর backup
docker compose up -d db
for i in $(seq 1 30); do
docker compose exec -T db pg_isready -U "$POSTGRES_USER" > /dev/null 2>&1 && break
sleep 2
done
mkdir -p ~/backups
docker compose exec -T db \
pg_dump --clean --if-exists -U "$POSTGRES_USER" "$POSTGRES_DB" \
> ~/backups/pre-$NEW_TAG.sql
# শেষ ১০টি backup রাখি, বাকিগুলো মুছি
ls -1t ~/backups/*.sql | tail -n +11 | xargs -r rm --
# ৩. migration (নতুন image দিয়ে চলবে)
docker compose run --rm api npx prisma migrate deploy
# ৪. দুটি app চালু
docker compose up -d
# ৫. সত্যিই চলছে কিনা, ৩০ সেকেন্ড পর্যন্ত দেখি
for i in $(seq 1 10); do
if curl -fsS http://localhost:3000 > /dev/null \
&& curl -fsS http://localhost:4000/health > /dev/null; then
trap - ERR
echo "deploy ok"
docker image prune -af --filter "until=336h"
exit 0
fi
sleep 3
done
# ৬. এখানে পৌঁছানো মানে app উঠল না
echo "health check failed"
rollback
exit 1your-username দুই জায়গায় আপনার GitHub username দিয়ে বদলান, এবং সবটুকু ছোট হাতের অক্ষরে লিখুন। Registry বড় হাতের অক্ষর নেয় না।
Script-এর পাঁচটি অংশের ব্যাখ্যা দরকার।
trap rollback ERR
.env-এ নতুন TAG বসানো হয় একদম শুরুতে। এরপর যেকোনো ধাপ fail করতে পারে — pull, backup, migration। trap না থাকলে set -e সেখানেই script থামিয়ে দিত, আর .env-এ এমন একটি version লেখা থেকে যেত যা কখনো চালুই হয়নি। trap নিশ্চিত করে যে কোথায় fail করল তা নির্বিশেষে .env আগের অবস্থায় ফেরে।
docker compose up -d db backup-এর আগে
docker compose exec শুধু চলমান container-এ কাজ করে। প্রথম deploy-এ database container তখনো কখনো চালু হয়নি, তাই exec সরাসরি fail করত। তাই আগে db চালু করে, pg_isready দিয়ে সত্যিই প্রস্তুত হওয়া পর্যন্ত অপেক্ষা করে, তারপর backup নেওয়া হয়।
pg_dump --clean --if-exists
এই দুটি flag ছাড়া dump file-এ শুধু CREATE TABLE আর INSERT লেখা থাকে — কোনো DROP থাকে না। তখন পরে ওই file ফেরাতে গিয়ে দেখবেন database-এ table গুলো আগে থেকেই আছে, আর পুরো restore relation already exists আর duplicate key error-এ ভরে গিয়ে অর্ধেক পথে পড়ে থাকে। --clean প্রতিটি table আগে মুছে দেয়, আর --if-exists নিশ্চিত করে যে table না থাকলেও error আসবে না।
command_timeout: 20m
appleboy/ssh-action default-এ ১০ মিনিট পরে হাল ছেড়ে দেয়। আগের page-এ শুধু pull আর restart ছিল, তাই লাগেনি। এখানে backup আর migration যোগ হয়েছে — database বড় হলে সময় বেশি লাগতে পারে, তাই সীমা বাড়ানো।
grep দিয়ে দুটি value পড়া
পুরো .env file shell-এ load না করে শুধু দরকারি দুটি লাইন পড়া হয়েছে। পুরোটা load করলে password-এ কোনো space, # বা quote থাকলে shell সেটি ভুল বুঝত — #-এর পরের অংশ comment ধরে বাদ দিয়ে দিত।
তবু database password-এ বিচিত্র অক্ষর না রাখাই ভালো, কারণ compose, shell আর Prisma-র connection string — তিন জায়গাতেই সেটি পড়া হয়। Password লম্বা করুন, জটিল নয়:
openssl rand -hex 32-hex দিলে শুধু 0-9 আর a-f আসে। -base64 দেবেন না — সেটি +, / আর = তৈরি করে, যা URL-এর ভেতরে সমস্যা করে।
The Health Endpoint
API-তে একটি health route রাখুন। এটি শুধু বলবে server জীবিত আছে:
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' })
})এর ভেতরে database query রাখবেন না। রাখলে database একটু ধীর হলেই health check fail করবে, আর pipeline অকারণে rollback করে বসবে।
Rollback Limits
Rollback পুরোনো image ফিরিয়ে আনে। কিন্তু database ফেরায় না।
Rollback করে: web আর api container পুরোনো image-এ ফিরে যায়
Rollback করে না: migration যা বদলেছে তা বদলানোই থাকেএই কারণেই উপরের “Safe Migrations” নিয়মগুলো এত জরুরি। Migration যদি পুরোনো code-এর সাথেও কাজ করে, তাহলে rollback-এর পরেও site স্বাভাবিক চলবে।
Migration যদি destructive হয় — column drop করা, table মুছে ফেলা — তখন rollback করেও লাভ নেই। সেই অবস্থায় ~/backups-এ রাখা file-ই একমাত্র ভরসা:
প্রথমে server-এ SSH করে দেখুন কোন backup ফেরাবেন:
ls -1t ~/backupsতারপর সেই file-টির নাম বসিয়ে চালান:
cd /opt/myapp
POSTGRES_USER=$(grep '^POSTGRES_USER=' .env | cut -d= -f2-)
POSTGRES_DB=$(grep '^POSTGRES_DB=' .env | cut -d= -f2-)
cat ~/backups/pre-a3f9c21.sql | docker compose exec -T db \
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" "$POSTGRES_DB"ON_ERROR_STOP=1 অংশটি বাদ দেবেন না। এটি ছাড়া psql error পেলেও পরের লাইনে চলে যায়, আর শেষে দেখে মনে হয় সব ঠিকঠাক হয়েছে — অথচ ভেতরে অর্ধেক data ফেরেনি। এটি দিলে প্রথম সমস্যাতেই থেমে যাবে।
অর্ধেক ফেরানো database-এর চেয়ে একটি পরিষ্কার error অনেক ভালো, কারণ তখন অন্তত আপনি জানেন কাজটি হয়নি।
Backup থেকে ফেরানোর মানে হলো backup নেওয়ার পর থেকে আসা সব নতুন data হারানো। এটি শেষ উপায়, প্রথম নয়। তাই destructive migration এড়িয়ে চলাই আসল সমাধান।
Keeping Images Small
দুটি image মানে registry-তে দ্বিগুণ জায়গা। Package private রাখলে GitHub-এর free plan-এ সীমা আছে — ৫০০ MB storage, মাসে ১ GB transfer। (Setup page-এ public করার কথা আছে; public হলে কোনো সীমা নেই।)
দুটি কথা মনে রাখলে জায়গা নিয়ে খুব বেশি ভাবতে হবে না:
node:20-alpineব্যবহার করুন,node:20নয়। সাধারণটির base প্রায় ১ GB, alpine-এর ১৩৫ MB-র কাছাকাছি- Layer share হয়। ২০টি tag মানে ২০ গুণ জায়গা নয় — শুধু বদলানো অংশটুকু নতুন করে জমা হয়
তবু কয়েক মাস deploy করতে থাকলে version জমে যাবে। তখন পুরোনোগুলো মুছতে হবে।
Deleting Old Versions
GHCR-এ পুরোনো version নিজে থেকে মুছে ফেলার কোনো retention setting নেই। অনেকে খুঁজে না পেয়ে বিভ্রান্ত হন — Actions-এর artifact-এর জন্য ওই setting আছে, package-এর জন্য নেই।
দুটি উপায় আছে। হাতে মুছতে চাইলে package-এর Versions পাতায় প্রতিটি version-এর পাশে একটি Delete বাটন আছে।
নিজে থেকে হওয়াতে চাইলে build job-এর শেষে এই দুটি step যোগ করুন:
- name: Clean up old web versions
uses: actions/delete-package-versions@v5
with:
package-name: myapp-web
package-type: container
min-versions-to-keep: 10
- name: Clean up old api versions
uses: actions/delete-package-versions@v5
with:
package-name: myapp-api
package-type: container
min-versions-to-keep: 10প্রতিটি deploy-এর পরে শেষ ১০টি version রেখে বাকিগুলো মুছে যাবে। দুটি image, তাই দুইবার লিখতে হলো। packages: write অনুমতি ওই job-এ আগে থেকেই আছে, তাই আর কিছু লাগবে না।
min-versions-to-keep ১০-এর চেয়ে কম দেবেন না। এটিই আপনার rollback-এর পথ — যত version রাখবেন, তত পিছনে ফিরে যেতে পারবেন।
Troubleshooting
সবচেয়ে বেশি যেসব error আসে:
| Error | কারণ | সমাধান |
|---|---|---|
Permission denied (publickey) | Secret-এ key ভুল, বা server-এ public key নেই | ssh -i ~/.ssh/gh_actions user@ip দিয়ে হাতে test করুন |
error in libcrypto | Private key-র content অসম্পূর্ণ copy হয়েছে | BEGIN থেকে END পর্যন্ত সব, শেষের খালি লাইনসহ আবার paste করুন |
denied: permission_denied push-এ | Job-এ packages: write অনুমতি নেই | permissions: block যোগ করুন |
manifest unknown pull-এ | Image ওই tag-এ নেই, বা নামে বড় হাতের অক্ষর আছে | নাম ছোট হাতের কিনা দেখুন, GHCR-এ tag আছে কিনা মিলিয়ে নিন |
unauthorized pull-এ | Server-এ GHCR login নেই বা token-এর মেয়াদ শেষ | docker login ghcr.io আবার করুন |
Can't reach database server at localhost | DATABASE_URL-এ localhost লেখা | db লিখুন — compose service-এর নাম |
Environment variable not found: DATABASE_URL | .env নেই বা env_file লাইন বাদ | /opt/myapp/.env আছে কিনা দেখুন |
| Migration চলল কিন্তু app ভাঙা | Migration পুরোনো code-এর সাথে খাপ খায় না | Expand পদ্ধতি ব্যবহার করুন |
prisma: not found container-এ | prisma এখনো devDependencies-এ আছে | dependencies-এ সরান |
| Deploy-এর পরে database খালি | compose-এ volume নেই | db-data volume যোগ করুন |
| Job সবুজ কিন্তু site পুরোনো | set -e নেই, মাঝপথে fail হয়েছে | set -e যোগ করুন, log আবার পড়ুন |
| Disk full হয়ে গেছে | পুরোনো image জমে আছে | docker image prune -af --filter "until=336h" চালান — ৩৩৬ ঘণ্টা মানে ১৪ দিন, দরকারে কমিয়ে দিন |
Restore-এ relation already exists | Dump-এ --clean --if-exists ছিল না | নতুন backup ওই দুটি flag দিয়ে নিন। পুরোনো file ফেরাতে আগে database drop করতে হবে |
প্রথম deploy-এ service "db" is not running | Database container কখনো চালু হয়নি | Script-এ docker compose up -d db লাইনটি আছে কিনা দেখুন |
Cannot use import statement outside a module | Runner stage-এ package.json copy হয়নি | Dockerfile-এ ওই COPY লাইনটি যোগ করুন |
Container বারবার Restarting | App ভেতরে crash করছে | docker compose logs -f দিয়ে আসল error দেখুন |
| Reboot-এর পরে site উঠল না | Docker service enabled নয় | sudo systemctl enable docker |
| Deploy সফল কিন্তু browser-এ পুরোনো | সামনে CDN পুরোনো copy দিচ্ছে | CDN-এর cache purge করুন |
Log পড়ার দুটি জায়গা আছে, গুলিয়ে ফেলবেন না:
- GitHub Actions-এর log — pipeline কোথায় থেমেছে। প্রথম লাল X-এ click করে শেষ ২০ লাইন পড়ুন
docker compose logs -f— app-এর নিজের error। Container চালু হয়েও যদি কাজ না করে, উত্তর এখানেই
Pipeline সবুজ অথচ site ভাঙা — এমন হলে দ্বিতীয়টি দেখুন।
Real-World Note
আপনার pipeline এখন database-এর চাবি ধরে আছে।
Deploy script pg_dump চালায় এবং migration চালায়। অর্থাৎ যে workflow file বদলাতে পারে, সে আপনার পুরো production database পড়তে বা মুছতে পারে।
Repository-তে write access আছে এমন যে কেউ deploy.yml-এ একটি লাইন যোগ করে পুরো database নিজের কাছে পাঠিয়ে নিতে পারে। এটি কোনো কল্পনা নয় — এটিই সবচেয়ে সহজ পথ।
চারটি জিনিস করুন:
mainbranch protect করুন। Settings → Branches → সরাসরি push বন্ধ, review বাধ্যতামূলক। এটি একটিমাত্র সবচেয়ে কার্যকর পদক্ষেপ।- Backup file গুলো server থেকে সরান।
~/backupsserver-এই আছে। Server hack হলে backup-ও গেল। প্রতিদিন সেগুলো অন্য জায়গায় copy করুন, আর যেহেতু এতে সব user-এর data আছে, encrypt করে রাখুন। - Database container-এর port বাইরে খুলবেন না। উপরের compose file-এ
db-র কোনোportsনেই — এটি ইচ্ছাকৃত। Debug করার জন্য একবার খুলে বন্ধ করতে ভুলে যাওয়া একটি খুব সাধারণ ভুল, আর automated scanner খোলা 5432 port কয়েক ঘণ্টার মধ্যে খুঁজে পায়। - Deploy log-এ কী যাচ্ছে দেখুন।
echo $DATABASE_URLজাতীয় debug লাইন ভুলে থেকে গেলে password log-এ চলে আসবে, আর Actions log মুছে ফেলা যায় না।
একটি দ্রুত পরীক্ষা: repository-র Settings → Collaborators খুলুন। ওই list-এর প্রত্যেকের হাতে এখন আপনার production database আছে।
Quick Check
- Pipeline-এ
prisma migrate devনেই তো? - Migration কেন
docker compose up -d-র ঠিক আগে থাকে? - Column-এর নাম বদলাতে কেন তিনটি deploy লাগে?
-
DATABASE_URL-এ host-এর নাম কী হবে? -
db-datavolume না দিলে কী হারাবেন? - Rollback database ফেরায় — সত্য না মিথ্যা?
আপনার pipeline এখন production-ready। Push করলে দুটি image build হয়, registry-তে যায়, database-এর backup হয়, migration চলে, container বদলায়, আর কিছু ভাঙলে নিজে থেকে পুরোনো image-এ ফিরে যায়।
Docker-এর ভেতরের বিষয়গুলো আরো গভীরে জানতে চাইলে — image কীভাবে ছোট করবেন, volume কীভাবে কাজ করে, container-রা কীভাবে একে অপরকে খুঁজে পায় — Docker section দেখুন।
পরবর্তী → Docker