Skip to Content
DocsCI/CDFirst Pipeline

First Pipeline

এই page-এ একটি Next.js app-এর জন্য সম্পূর্ণ pipeline লিখবেন। শেষে git push করলেই GitHub image বানাবে, registry-তে পাঠাবে, আর আপনার server সেটি টেনে চালু করবে।

Before You Start

এই page সরাসরি পড়তে এলে নিচের জিনিসগুলো তৈরি থাকতে হবে:

  • Server-এ Docker আছে, /opt/myapp folder আছে এবং আপনার user-এর মালিকানায়
  • GitHub-এ তিনটি secret আছে — SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY
  • Server-এ docker login ghcr.io করা আছে

কোনোটি না থাকলে Setup page-এ সব দেওয়া আছে।

What Goes Where

শুরুতেই পরিষ্কার করে নিই কোন file কোথায় থাকবে। এই ছবিটি মাথায় থাকলে বাকি ধাপগুলো পরিষ্কার লাগবে।

GitHub repository-তে (আপনি যা লেখেন):

myapp/ ├── .github/workflows/ │ ├── ci.yml │ └── deploy.yml ├── Dockerfile ├── .dockerignore ├── next.config.mjs └── package.json

Server-এ (মাত্র দুটি file):

/opt/myapp/ ├── compose.yaml └── .env

Server-এ কোনো code নেই। কোনো Dockerfile নেই। কোনো node_modules নেই।

Turn On Standalone Output

Next.js-এর একটি setting আছে যা image-কে অনেক ছোট করে দেয়। এটি চালু না করলে আপনার image-এ পুরো node_modules folder ঢুকে যাবে।

output: 'standalone' দিলে Next.js build-এর সময় হিসাব করে দেখে ঠিক কোন file গুলো চালাতে লাগবে, আর শুধু সেগুলো একটি folder-এ জড়ো করে। বাকি সব বাদ।

ফলটা স্পষ্ট — সাধারণ Next.js image প্রায় ১ GB, standalone দিয়ে সেটি ২০০ MB-র কাছাকাছি নেমে আসে। এর মধ্যে node:20-alpine base image-ই প্রায় ১৩৫ MB, বাকিটুকু আপনার app।

প্রতিবার deploy-এ server-কে এই image টানতে হয়, তাই ছোট image মানে দ্রুত deploy আর কম bandwidth।

next.config.mjs
const nextConfig = { output: 'standalone', } export default nextConfig

File-এর extension .mjs, .js নয়। .js file-এ export default লিখলে Node সেটি পড়তে পারে না — package.json-এ "type": "module" না থাকলে build fail করবে। .mjs দিলে এই ঝামেলা থাকে না।

আপনার project-এ যদি আগে থেকেই next.config.js থাকে এবং সেটি module.exports ব্যবহার করে, তাহলে নাম না বদলে শুধু ভেতরে output: 'standalone' লাইনটি যোগ করুন।

Write the Dockerfile

এই Dockerfile-এ তিনটি stage আছে। Stage মানে ধাপ — প্রতিটি ধাপ আগের ধাপ থেকে শুধু দরকারি জিনিসটুকু নিয়ে আসে, বাকি সব ফেলে দেয়।

Dockerfile
# ধাপ ১: শুধু dependency install FROM node:20-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci # ধাপ ২: build FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # ধাপ ৩: শুধু চালানোর জন্য যা লাগে FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production # root হিসেবে না চালানোর জন্য একটি সাধারণ user RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME=0.0.0.0 CMD ["node", "server.js"]

তিনটি stage কেন — শেষ stage-এ শুধু public, .next/standalone আর .next/static copy করা হয়েছে। node_modules, source code, build tool — কিছুই যায়নি। ওগুলো আগের stage-এ ছিল, আর সেই stage গুলো final image-এ থাকে না।

শেষের দুটি লাইন বাদ দেবেন না।

ENV HOSTNAME=0.0.0.0

Docker প্রতিটি container-এ HOSTNAME নামে একটি variable বসিয়ে দেয়, যার value হলো container-এর নিজের id। Next.js-এর তৈরি server.js ওই variable দেখেই ঠিক করে কোন ঠিকানায় সে সাড়া দেবে।

এই লাইনটি না দিলে সে ভুল ঠিকানায় বসে থাকে, আর বাইরে থেকে কোনো connection পৌঁছায় না। 0.0.0.0 মানে “সব ঠিকানায় সাড়া দাও”। Next.js-এর official Dockerfile-এও এই লাইনটি আছে।

USER nextjs

এটি না থাকলে আপনার app container-এর ভেতরে root হিসেবে চলবে। কেউ app-এর কোনো দুর্বলতা কাজে লাগিয়ে ভেতরে ঢুকে পড়লে তার হাতে container-এর সব ক্ষমতা চলে যাবে।

Add a .dockerignore

COPY . . লাইনটি পুরো folder copy করে। .dockerignore না থাকলে সেখানে .git, node_modules, এমনকি আপনার local .env file-ও ঢুকে যাবে।

.dockerignore
node_modules .next .git .env .env.* README.md Dockerfile .dockerignore

.env লাইনটি সবচেয়ে গুরুত্বপূর্ণ। এটি না থাকলে আপনার সব secret image-এর ভেতরে ঢুকে যাবে, আর সেই image registry-তে চলে যাবে। Image-এর layer খুলে যে কেউ সেগুলো পড়তে পারে।

Create the CI File

এই file আপনার প্রথম পাহারা। কেউ pull request খুললে GitHub image build করে দেখবে। Build fail করলে pull request-এ লাল X দেখাবে এবং merge করার আগেই সমস্যা ধরা পড়বে।

Registry-তে কিছু পাঠানো হবে না — শুধু build করে দেখা।

.github/workflows/ci.yml
name: CI on: pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - name: Build image uses: docker/build-push-action@v6 with: context: . push: false cache-from: type=gha cache-to: type=gha,mode=max

cache-from আর cache-to দুটি লাইন Docker-এর layer গুলো জমা রাখে। প্রথম build-এ কিছু হবে না, কিন্তু পরের বার থেকে যে layer গুলো বদলায়নি সেগুলো আর নতুন করে বানাতে হবে না। Build-এর সময় প্রায় অর্ধেক নেমে আসে।

Create the Server Files

এবার server-এ যান এবং দুটি file তৈরি করুন।

প্রথমে compose.yaml:

SERVER — Terminal
nano /opt/myapp/compose.yaml
/opt/myapp/compose.yaml
services: web: image: ghcr.io/your-username/myapp:${TAG} restart: unless-stopped ports: - "127.0.0.1:3000:3000" environment: NODE_ENV: production

your-username আপনার GitHub username দিয়ে বদলান, এবং সবটুকু ছোট হাতের অক্ষরে লিখুন। আপনার username-এ বড় হাতের অক্ষর থাকলেও registry নাম ছোট হাতের ছাড়া নেয় না। এই এক জায়গায় অনেকে আটকে যান।

তিনটি জরুরি লাইন:

  • ${TAG} — কোন version চলবে তা .env file থেকে আসবে। এটিই rollback-এর চাবি
  • restart: unless-stopped — container crash করলে বা server reboot হলে Docker নিজে থেকে আবার চালু করবে
  • 127.0.0.1:3000:3000 — port শুধু server-এর ভেতরে খোলা, internet থেকে সরাসরি ঢোকা যাবে না। বাইরের traffic যা সামনে বসানো আছে (যেমন Nginx) তার মধ্য দিয়ে আসবে

এবার .env:

SERVER — Terminal
nano /opt/myapp/.env
/opt/myapp/.env
TAG=latest

এখন latest লিখে রাখুন। প্রথম deploy-এর পরে pipeline নিজেই এই লাইনটি commit-এর নাম দিয়ে বদলে দেবে।

Server-এ file-টির permission শক্ত করে দিন:

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

এখন এতে গোপন কিছু নেই। কিন্তু পরে database password বা API key যোগ করবেন, তাই অভ্যাসটা শুরু থেকেই রাখা ভালো।

খেয়াল করুন — compose file-এ env_file লাইন নেই, তবু ${TAG} কাজ করছে। কারণ Docker Compose একই folder-এর .env নিজে থেকেই পড়ে নাম বসানোর জন্য

env_file অন্য কাজ করে — সে .env-এর সব value container-এর ভেতরে পাঠায়। এখানে সেটি দরকার নেই, তাই দেওয়া হয়নি। Container যতটুকু না জানলেও চলে, ততটুকু না দেওয়াই ভালো অভ্যাস।

Create the Deploy File

এবার আসল কাজ।

.github/workflows/deploy.yml
name: Deploy on: push: branches: [main] # একসাথে দুইটি deploy চলতে দেবে না concurrency: group: production-deploy cancel-in-progress: false env: IMAGE: ghcr.io/your-username/myapp 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 uses: docker/build-push-action@v6 with: context: . push: true tags: | ${{ env.IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:latest cache-from: type=gha cache-to: type=gha,mode=max # 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 }} script: | set -eE cd /opt/myapp # এখন কোন version চলছে, মনে রাখি — ফিরে আসার জন্য 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 বসাই sed -i "s|^TAG=.*|TAG=$NEW_TAG|" .env docker compose pull docker compose up -d # সত্যিই চলছে কিনা দেখি, ৩০ সেকেন্ড পর্যন্ত অপেক্ষা for i in $(seq 1 10); do if curl -fsS http://localhost:3000 > /dev/null; then trap - ERR echo "deploy ok" # ১৪ দিনের পুরোনো, অব্যবহৃত image মুছে দিই docker image prune -af --filter "until=336h" exit 0 fi sleep 3 done # এখানে পৌঁছানো মানে app উঠল না echo "health check failed" rollback exit 1

How TAG Works

এই pipeline-এর পুরো বুদ্ধিটা .env file-এর ওই একটি লাইনে।

/opt/myapp/.env
TAG=a3f9c21

Docker Compose নিজে থেকেই একই folder-এর .env file পড়ে নেয়। তাই compose.yaml-এ লেখা ${TAG} জায়গায় সে a3f9c21 বসিয়ে দেয়:

image: ghcr.io/your-username/myapp:${TAG} image: ghcr.io/your-username/myapp:a3f9c21

Deploy করার মানে দাঁড়ায় শুধু ওই এক লাইন বদলে docker compose up -d চালানো।

আর rollback করার মানে দাঁড়ায় ওই এক লাইনে পুরোনো নামটি ফিরিয়ে আনা। পুরোনো image সাধারণত আগে থেকেই server-এ নামানো থাকে, তাই কিছু টানতে হয় না — দশ সেকেন্ডের মধ্যেই পুরোনো version ফিরে আসে।

The Health Check

Deploy-এর শেষে ওই for loop-টি বাদ দেবেন না।

মূল কারণ: GitHub-এর সবুজ checkmark-এর মানে শুধু “সব command exit code 0 দিয়েছে”। docker compose up -d container চালু করেই ফিরে আসে — container-টি ভেতরে গিয়ে crash করল কিনা সে জানে না।

App উঠে গিয়ে তিন সেকেন্ড পরে মরে যেতে পারে — .env-এ একটি variable missing থাকলে, port দখল হয়ে থাকলে, বা code-এ কোনো ভুল থাকলে। Docker তখনো “started” বলবে, GitHub সবুজ দেখাবে, আর আপনার site down থাকবে।

curl তাই সরাসরি site-এ গিয়ে দেখে সত্যিই সাড়া আসছে কিনা।

curl flag গুলোর মানে:

  • -f — HTTP error (৪০০, ৫০০) পেলে fail করবে। এটি ছাড়া error page-ও “সফল” গণ্য হয়
  • -s — progress bar লুকায়
  • -S — তবু error message দেখায়

Loop-টি ১০ বার চেষ্টা করে, প্রতিবার ৩ সেকেন্ড অপেক্ষা করে। Next.js উঠতে সাধারণত ৩-৫ সেকেন্ড লাগে, তাই ৩০ সেকেন্ড যথেষ্ট।

Automatic Rollback

Script-এ rollback নামে একটি function আছে আর তার সাথে একটি লাইন:

trap rollback ERR

trap shell-কে বলে — “নিচের যেকোনো command fail করলে আগে rollback চালিয়ে নিও”।

কেন দরকার — script-এর শুরুতেই .env-এ নতুন TAG বসিয়ে দেওয়া হয়। এরপর যদি docker compose pull fail করে (registry-তে ঢুকতে না পারলে, বা image না পেলে), তাহলে set -e-র কারণে script সাথে সাথে থেমে যাবে।

trap না থাকলে তখন যা হতো: .env-এ এমন একটি version লেখা আছে যা কখনো চালুই হয়নি। পরের বার কেউ docker compose up -d দিলে বা server reboot হলে সেই ভুল version উঠতে চাইবে।

trap থাকায় যেখানেই fail করুক, .env আগের অবস্থায় ফিরে যায়।

Health check পাশ করলে trap - ERR দিয়ে trap-টি সরিয়ে দেওয়া হয় — কাজ শেষ, আর দরকার নেই।

প্রথম deploy-এ rollback কাজ করবে না, আর সেটাই স্বাভাবিক।

কারণ .env-এ শুরুতে TAG=latest লেখা ছিল। latest কোনো নির্দিষ্ট version নয় — আর এই run-এ সেটিও নতুন (ভাঙা) image-এর দিকেই ঘুরে গেছে। তাই ওখানে ফিরে যাওয়ার কোনো মানে হয় না।

rollback function-এর শুরুর if লাইনটি ঠিক এই কারণেই আছে — সে দেখে নেয় ফেরার মতো সত্যিকারের কোনো version আছে কিনা, না থাকলে পরিষ্কার করে বলে দেয়।

দ্বিতীয় deploy থেকে rollback পুরোপুরি কাজ করবে, কারণ তখন .env-এ প্রথম deploy-এর commit sha লেখা থাকবে।

Disk Cleanup

Health check পাশ করার পরে script-এর শেষ কাজটি এই লাইনটি:

docker image prune -af --filter "until=336h"

প্রতিটি deploy server-এ একটি নতুন image নামায়, কিন্তু পুরোনোগুলো নিজে থেকে মোছে না। কয়েক মাস পরে disk ভরে যায়, আর তখন no space left on device দিয়ে deploy fail করতে শুরু করে।

Flag তিনটির মানে:

  • -a — কোনো container ব্যবহার করছে না, এমন সব image ধরবে
  • -f — “সত্যিই মুছব?” জিজ্ঞেস করবে না। Pipeline-এ কেউ উত্তর দেওয়ার নেই
  • until=336h১৪ দিনের চেয়ে পুরোনো হলেই মুছবে। ৩৩৬ ঘণ্টা = ১৪ দিন

চলমান container যে image ব্যবহার করছে সেটি কখনো মোছে না, তাই এটি নিরাপদ।

এই লাইনটি rollback-এর সাথে একটু টানাপোড়েনে আছে, জেনে রাখা ভালো।

Rollback দ্রুত হয় কারণ পুরোনো image আগে থেকেই server-এ নামানো থাকে। কিন্তু prune ১৪ দিনের পুরোনো image মুছে দেয়।

রোজ deploy করলে কোনো সমস্যা নেই — আগের image সবসময়ই নতুন। কিন্তু মাসে একবার deploy করলে আগের image ততদিনে মুছে গেছে।

তখনো rollback কাজ করবে, কারণ docker compose up -d image না পেলে নিজে থেকে registry থেকে টেনে নেয়। শুধু দশ সেকেন্ডের বদলে আধ মিনিট লাগবে।

কম deploy করলে সময়সীমা বাড়িয়ে দিন — until=2160h মানে ৯০ দিন।

Concurrency

concurrency: group: production-deploy cancel-in-progress: false

ধরুন আপনি push করলেন, তারপর ৩০ সেকেন্ড পরে একটি ছোট typo ঠিক করে আবার push করলেন। concurrency না থাকলে দুটি deploy একসাথে চলবে।

তখন দুটি script একই .env file-এ একই সময়ে লিখতে যাবে। ফলাফল অনুমান করা যায় না — কোন version শেষ পর্যন্ত চলবে কেউ জানে না।

concurrency দ্বিতীয় deploy-কে লাইনে দাঁড় করিয়ে রাখে। cancel-in-progress: false লেখা আছে কারণ deploy মাঝপথে বাতিল করা বিপজ্জনক।

Push and Watch

সব file commit করে push করুন:

LOCAL — Terminal
git add . git commit -m "add docker ci cd pipeline" git push origin main

GitHub-এ Actions tab খুলুন। সাথে সাথেই একটি run শুরু হবে।

  • build job চলবে — প্রথমবার ৩-৫ মিনিট (cache খালি), পরের বার থেকে ১-২ মিনিট
  • সবুজ হলে deploy job শুরু হবে — প্রায় ৩০ সেকেন্ড

Package public করবেন ঠিক করে থাকলে প্রথমবার deploy job fail করবে — এটি স্বাভাবিক।

Package তৈরিই হলো এইমাত্র, তাই এখনো private অবস্থায় আছে। এখন GitHub-এ গিয়ে সেটিকে public করুন, তারপর Actions-এ Re-run failed jobs চাপুন।

কেন এমন হয় তার পুরো ব্যাখ্যা Setup page-এ আছে।

Server-এ গিয়ে মিলিয়ে দেখুন:

SERVER — Terminal
cd /opt/myapp cat .env docker compose ps

.env-এ TAG=latest-এর বদলে একটি লম্বা commit sha দেখা যাবে। ps-এ container Up অবস্থায় থাকবে।

Up না দেখালে, বা Restarting দেখালে — server-এ থেকেই container-এর ভেতরে কী হচ্ছে দেখুন:

SERVER — Terminal
docker compose logs -f

এখানেই আপনার app-এর নিজের error message পাবেন। Pipeline-এর log ভেতরের কথা বলে না — সে শুধু জানে container চালু হয়েছিল কিনা।

Test It for Real

Pipeline সত্যিই কাজ করছে কিনা জানার একটিই উপায় — একটি পরিবর্তন করে দেখা।

Homepage-এর কোনো লেখা বদলান, তারপর:

LOCAL — Terminal
git add . git commit -m "update homepage text" git push origin main

এখন কিছুই করবেন না। শুধু ২ মিনিট অপেক্ষা করে browser refresh দিন। নতুন লেখা দেখা যাবে।

আপনি আর কখনো হাতে deploy করবেন না।

এবার server-এর পুরোনো clone করা project folder-টি মুছে ফেলতে পারেন। সেটির আর কোনো কাজ নেই। Server-এ এখন শুধু ওই দুটি file আর Docker।

Will It Survive a Reboot

আরেকটি পরীক্ষা বাকি, আর এটি অনেকে বাদ দিয়ে দেন।

Server তো মাঝে মাঝে reboot হবেই — update, maintenance, বা cloud provider-এর কারণে। তখন container নিজে থেকে উঠবে কিনা?

Server-এ দুটি জিনিস ঠিক থাকতে হবে:

SERVER — Terminal
systemctl is-enabled docker docker inspect $(docker compose ps -q) --format '{{.HostConfig.RestartPolicy.Name}}'

প্রথমটি enabled আর দ্বিতীয়টি unless-stopped দেখালে ঠিক আছে।

  • enabled না দেখালে — sudo systemctl enable docker চালান
  • unless-stopped না দেখালে — compose file-এ ওই লাইনটি বাদ পড়েছে

পড়ে নিশ্চিত হওয়া আর পরীক্ষা করে নিশ্চিত হওয়া এক নয়। সময় থাকলে সত্যিই একবার sudo reboot দিন, তারপর দুই মিনিট পরে site খুলে দেখুন।

এখন এক মিনিট down থাকা ভালো, নাকি রাত ৩টায় server reboot হয়ে সারা রাত down থাকা?

Reaching It from the Internet

Compose file-এ port লেখা আছে 127.0.0.1:3000:3000। এর মানে container-টি শুধু server-এর ভেতর থেকে পাওয়া যায়। Browser-এ আপনার domain লিখলে এখনো কিছুই আসবে না।

এটি ইচ্ছাকৃত। Node.js সরাসরি internet-এর মুখোমুখি বসার জন্য তৈরি নয় — HTTPS certificate, একাধিক domain, ধীর client — এসব সে ভালোভাবে সামলায় না।

তাই মাঝখানে একটি reverse proxy বসে। বাইরের সব traffic সে নেয়, তারপর ভেতরে container-এর কাছে পাঠায়:

Internet → Nginx (port 80, 443) → container (127.0.0.1:3000)

সবচেয়ে ছোট Nginx config যা কাজ করে:

SERVER — /etc/nginx/sites-available/myapp
server { listen 80; server_name yourdomain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }

Server-এ এটি চালু করুন:

SERVER — Terminal
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx

sudo nginx -t অংশটি config-এ ভুল আছে কিনা দেখে নেয়। ভুল থাকলে reload হবে না, তাই চলমান site ভাঙে না।

তারপর HTTPS বসাতে দুটি command:

SERVER — Terminal
sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d yourdomain.com

Certbot নিজেই certificate এনে Nginx config-এ বসিয়ে দেয়, আর মেয়াদ শেষ হওয়ার আগে নিজে থেকে renew করে।

Nginx আর certificate একবার বসিয়ে দিলে আর ছোঁয়ার দরকার নেই। Deploy-এ শুধু container বদলায়, Nginx আগের মতোই 127.0.0.1:3000-এ পাঠাতে থাকে।

এখানে শুধু কাজ চালানোর মতো টুকু দেওয়া হলো। বিস্তারিত আছে Nginx আর SSL / HTTPS page-এ।

When a CDN Is in Front

Site-এর সামনে Cloudflare বা অন্য কোনো CDN থাকলে একটি জিনিস জেনে রাখুন।

Deploy সফল, container ঠিক চলছে, server সঠিক উত্তর দিচ্ছে — তবু browser-এ পুরোনো জিনিস দেখাচ্ছে। কারণ CDN মাঝখানে বসে পুরোনো copy দিচ্ছে।

সাধারণত HTML cache হয় না, তাই লেখার পরিবর্তন সাথে সাথেই দেখা যায়। কিন্তু .js, .css আর ছবি cache হয়।

Server সত্যিই কী দিচ্ছে দেখতে চাইলে URL-এর শেষে একটি অর্থহীন query যোগ করুন — CDN তখন এটিকে নতুন ঠিকানা ভেবে server-এ জিজ্ঞেস করবে:

LOCAL — Terminal
curl -s -o /dev/null -w "cached: %{http_code}\n" https://yoursite.com/app.js curl -s -o /dev/null -w "fresh: %{http_code}\n" "https://yoursite.com/app.js?x=123"

দুটি আলাদা উত্তর এলে বুঝবেন সমস্যা আপনার server-এ নয়, CDN-এ। তখন CDN-এর dashboard থেকে cache purge করুন।

একটি জিনিস অনেককে ভোগায় — CDN ভুল উত্তরও cache করে। কোনো file একবার 404 দিলে সেই 404-টাই জমা হয়ে যায়, কখনো কখনো এক বছরের জন্য।

পরে file-টি ঠিক করে দিলেও CDN পুরোনো 404 দিতেই থাকবে। Purge না করা পর্যন্ত ঠিক হবে না।

Real-World Note

Image-এর ভেতরে যা রাখেন তা সবাই পড়তে পারে।

একটি Docker image হলো layer-এর একটি stack। যে কেউ আপনার image টেনে নিয়ে প্রতিটি layer খুলে দেখতে পারে — কোন file ছিল, কোন file মুছে ফেলা হয়েছে, সব।

এর মানে দুটি জিনিস:

  • কখনো .env বা key file image-এ ঢোকাবেন না। .dockerignore ঠিক আছে কিনা নিশ্চিত হয়ে নিন। Build-এর সময় secret লাগলে Docker-এর --mount=type=secret ব্যবহার করুন, COPY নয়।
  • মুছে ফেললেও থেকে যায়। একটি layer-এ file যোগ করে পরের layer-এ RUN rm দিয়ে মুছলে সেটি আসলে মুছে যায় না — আগের layer-এ পড়ে থাকে। শুধু দেখা যায় না।

নিজের image-এ কী আছে দেখতে চাইলে:

LOCAL — Terminal
docker history ghcr.io/your-username/myapp:latest --no-trunc

আরেকটি জিনিস মনে রাখুন। NEXT_PUBLIC_ দিয়ে শুরু হওয়া প্রতিটি environment variable Next.js build-এর সময় JavaScript bundle-এর ভেতরে বসিয়ে দেয় এবং browser-এ পাঠায়। কেউ Ctrl+U চেপেই দেখতে পারে। কখনো database password বা API secret key-তে NEXT_PUBLIC_ prefix দেবেন না।

Quick Check

  • Pull request খুললে ci.yml নিজে থেকে চলছে?
  • main-এ push করলে site নিজে থেকে update হচ্ছে?
  • Server-এ এখন কোন দুটি file আছে?
  • .env-এর TAG লাইনটি কী কাজ করে?
  • Health check না থাকলে কোন সমস্যাটি ধরা পড়বে না?

আপনার Next.js app এখন নিজে থেকে deploy হচ্ছে, আর deploy fail করলে নিজে থেকে ফিরেও যাচ্ছে।

কিন্তু আসল production project-এ সাধারণত একটির বেশি জিনিস চলে — একটি আলাদা API server, একটি database, database-এর schema migration। Migration ভুল সময়ে চললে site ভেঙে যায়, আর তখন শুধু image ফিরিয়ে আনলেই সমস্যা মেটে না।

পরের page-এ এই pipeline-কে বাড়িয়ে সম্পূর্ণ production stack চালাব।

পরবর্তী → Production Pipeline

docker github actions deploy bangla, ghcr push image bangla, nextjs standalone docker bangla, docker compose deploy bangla, build push action bangla, docker multi stage build bangla, auto deploy docker bangla

Last updated on