Skip to Content
DocsCI/CDProduction Pipeline

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 folderSetup
GitHub-এ তিনটি secretSetup
Registry-তে access — package public, নয়তো server-এ loginSetup
একটি কাজ করা basic pipelineFirst Pipeline

Basic pipeline না বুঝে থাকলে আগে ওই page-টি দেখে নিন। এখানে সম্পূর্ণ file দেওয়া আছে ঠিকই, কিন্তু TAG কীভাবে কাজ করে, needs বা concurrency কেন লাগে — তার ব্যাখ্যা ওখানে আছে, এখানে আবার লেখা হয়নি।

What Changes

আগের pipeline একটি image deploy করত। এখন তিনটি নতুন জিনিস যোগ হলো।

First PipelineProduction 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 দিয়ে। পুরোনো web service-এর image-এর নামও বদলাবে — myapp থেকে myapp-web
  • .env-এ নতুন লাইনগুলো যোগ করুনPOSTGRES_* আর DATABASE_URLTAG লাইনটি যেমন আছে তেমনই থাকুক
  • পুরোনো myapp package-টি এখনই মুছবেন না। নতুন 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/apiDocker ওই folder-এর বাইরের কিছুই দেখতে পায় না। তাই দুটি জিনিস প্রতিটি app-এর ভেতরে থাকতে হবে:

  • নিজের package-lock.json — root-এ রাখলে npm ci সেটি খুঁজে পাবে না, build fail করবে
  • নিজের .dockerignore — root-এ রাখলে Docker সেটি পড়বেই না, আর আপনার local .env image-এর ভেতরে ঢুকে registry-তে চলে যাবে

দুটি app-এর .dockerignore-এ একই content:

apps/web/.dockerignore
node_modules .next dist .git .env .env.* Dockerfile .dockerignore

npm 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 আছে।

apps/api/Dockerfile
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। শুধু ভেতরে আরো কিছু যোগ হলো।

/opt/myapp/compose.yaml
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:

/opt/myapp/.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/myapp

DATABASE_URL-এ localhost নয়, db লেখা আছে। এটি compose file-এ দেওয়া service-এর নাম।

Container-এর ভেতর থেকে localhost মানে ওই container নিজে — database নয়। এই এক জায়গায় প্রায় সবাই একবার আটকায়।

এই file-এ database-এর password আছে। Server-এ গিয়ে permission শক্ত করে দিন, যাতে শুধু আপনার user পড়তে পারে:

SERVER — Terminal
chmod 600 /opt/myapp/.env

Docker 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-এ:

.github/workflows/deploy.yml
- 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-এ সেটি গ্রহণ করতে হয়:

apps/web/Dockerfile
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 devprisma 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-এ কাজের নিয়ম:

LOCAL — Terminal
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-এ।

.github/workflows/ci.yml
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। আগের সব আলোচনা এখানে একসাথে।

.github/workflows/deploy.yml
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 1

your-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 লম্বা করুন, জটিল নয়:

LOCAL — Terminal
openssl rand -hex 32

-hex দিলে শুধু 0-9 আর a-f আসে। -base64 দেবেন না — সেটি +, / আর = তৈরি করে, যা URL-এর ভেতরে সমস্যা করে।

The Health Endpoint

API-তে একটি health route রাখুন। এটি শুধু বলবে server জীবিত আছে:

apps/api/src/server.ts
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 ফেরাবেন:

SERVER — Terminal
ls -1t ~/backups

তারপর সেই file-টির নাম বসিয়ে চালান:

SERVER — Terminal
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 যোগ করুন:

.github/workflows/deploy.yml
- 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 libcryptoPrivate 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 localhostDATABASE_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 existsDump-এ --clean --if-exists ছিল নানতুন backup ওই দুটি flag দিয়ে নিন। পুরোনো file ফেরাতে আগে database drop করতে হবে
প্রথম deploy-এ service "db" is not runningDatabase container কখনো চালু হয়নিScript-এ docker compose up -d db লাইনটি আছে কিনা দেখুন
Cannot use import statement outside a moduleRunner stage-এ package.json copy হয়নিDockerfile-এ ওই COPY লাইনটি যোগ করুন
Container বারবার RestartingApp ভেতরে 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 নিজের কাছে পাঠিয়ে নিতে পারে। এটি কোনো কল্পনা নয় — এটিই সবচেয়ে সহজ পথ।

চারটি জিনিস করুন:

  • main branch protect করুন। Settings → Branches → সরাসরি push বন্ধ, review বাধ্যতামূলক। এটি একটিমাত্র সবচেয়ে কার্যকর পদক্ষেপ।
  • Backup file গুলো server থেকে সরান। ~/backups server-এই আছে। 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-data volume না দিলে কী হারাবেন?
  • Rollback database ফেরায় — সত্য না মিথ্যা?

আপনার pipeline এখন production-ready। Push করলে দুটি image build হয়, registry-তে যায়, database-এর backup হয়, migration চলে, container বদলায়, আর কিছু ভাঙলে নিজে থেকে পুরোনো image-এ ফিরে যায়।

Docker-এর ভেতরের বিষয়গুলো আরো গভীরে জানতে চাইলে — image কীভাবে ছোট করবেন, volume কীভাবে কাজ করে, container-রা কীভাবে একে অপরকে খুঁজে পায় — Docker section দেখুন।

পরবর্তী → Docker

prisma migrate deploy bangla, docker compose production bangla, ghcr private image bangla, database backup pipeline bangla, rollback docker image bangla, monorepo docker deploy bangla, express nextjs postgres docker bangla, ci cd production bangla

Last updated on