First Pipeline
এই page-এ একটি Next.js app-এর জন্য সম্পূর্ণ pipeline লিখবেন। শেষে git push করলেই GitHub image বানাবে, registry-তে পাঠাবে, আর আপনার server সেটি টেনে চালু করবে।
Before You Start
এই page সরাসরি পড়তে এলে নিচের জিনিসগুলো তৈরি থাকতে হবে:
- Server-এ Docker আছে,
/opt/myappfolder আছে এবং আপনার 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.jsonServer-এ (মাত্র দুটি file):
/opt/myapp/
├── compose.yaml
└── .envServer-এ কোনো 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।
const nextConfig = {
output: 'standalone',
}
export default nextConfigFile-এর 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 মানে ধাপ — প্রতিটি ধাপ আগের ধাপ থেকে শুধু দরকারি জিনিসটুকু নিয়ে আসে, বাকি সব ফেলে দেয়।
# ধাপ ১: শুধু 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-ও ঢুকে যাবে।
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 করে দেখা।
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=maxcache-from আর cache-to দুটি লাইন Docker-এর layer গুলো জমা রাখে। প্রথম build-এ কিছু হবে না, কিন্তু পরের বার থেকে যে layer গুলো বদলায়নি সেগুলো আর নতুন করে বানাতে হবে না। Build-এর সময় প্রায় অর্ধেক নেমে আসে।
Create the Server Files
এবার server-এ যান এবং দুটি file তৈরি করুন।
প্রথমে compose.yaml:
nano /opt/myapp/compose.yamlservices:
web:
image: ghcr.io/your-username/myapp:${TAG}
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
NODE_ENV: productionyour-username আপনার GitHub username দিয়ে বদলান, এবং সবটুকু ছোট হাতের অক্ষরে লিখুন। আপনার username-এ বড় হাতের অক্ষর থাকলেও registry নাম ছোট হাতের ছাড়া নেয় না। এই এক জায়গায় অনেকে আটকে যান।
তিনটি জরুরি লাইন:
${TAG}— কোন version চলবে তা.envfile থেকে আসবে। এটিই rollback-এর চাবিrestart: unless-stopped— container crash করলে বা server reboot হলে Docker নিজে থেকে আবার চালু করবে127.0.0.1:3000:3000— port শুধু server-এর ভেতরে খোলা, internet থেকে সরাসরি ঢোকা যাবে না। বাইরের traffic যা সামনে বসানো আছে (যেমন Nginx) তার মধ্য দিয়ে আসবে
এবার .env:
nano /opt/myapp/.envTAG=latestএখন latest লিখে রাখুন। প্রথম deploy-এর পরে pipeline নিজেই এই লাইনটি commit-এর নাম দিয়ে বদলে দেবে।
Server-এ file-টির permission শক্ত করে দিন:
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
এবার আসল কাজ।
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 1How TAG Works
এই pipeline-এর পুরো বুদ্ধিটা .env file-এর ওই একটি লাইনে।
TAG=a3f9c21Docker Compose নিজে থেকেই একই folder-এর .env file পড়ে নেয়। তাই compose.yaml-এ লেখা ${TAG} জায়গায় সে a3f9c21 বসিয়ে দেয়:
image: ghcr.io/your-username/myapp:${TAG}
↓
image: ghcr.io/your-username/myapp:a3f9c21Deploy করার মানে দাঁড়ায় শুধু ওই এক লাইন বদলে 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 ERRtrap 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 করুন:
git add .
git commit -m "add docker ci cd pipeline"
git push origin mainGitHub-এ Actions tab খুলুন। সাথে সাথেই একটি run শুরু হবে।
buildjob চলবে — প্রথমবার ৩-৫ মিনিট (cache খালি), পরের বার থেকে ১-২ মিনিট- সবুজ হলে
deployjob শুরু হবে — প্রায় ৩০ সেকেন্ড
Package public করবেন ঠিক করে থাকলে প্রথমবার deploy job fail করবে — এটি স্বাভাবিক।
Package তৈরিই হলো এইমাত্র, তাই এখনো private অবস্থায় আছে। এখন GitHub-এ গিয়ে সেটিকে public করুন, তারপর Actions-এ Re-run failed jobs চাপুন।
কেন এমন হয় তার পুরো ব্যাখ্যা Setup page-এ আছে।
Server-এ গিয়ে মিলিয়ে দেখুন:
cd /opt/myapp
cat .env
docker compose ps.env-এ TAG=latest-এর বদলে একটি লম্বা commit sha দেখা যাবে। ps-এ container Up অবস্থায় থাকবে।
Up না দেখালে, বা Restarting দেখালে — server-এ থেকেই container-এর ভেতরে কী হচ্ছে দেখুন:
docker compose logs -fএখানেই আপনার app-এর নিজের error message পাবেন। Pipeline-এর log ভেতরের কথা বলে না — সে শুধু জানে container চালু হয়েছিল কিনা।
Test It for Real
Pipeline সত্যিই কাজ করছে কিনা জানার একটিই উপায় — একটি পরিবর্তন করে দেখা।
Homepage-এর কোনো লেখা বদলান, তারপর:
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-এ দুটি জিনিস ঠিক থাকতে হবে:
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 {
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-এ এটি চালু করুন:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxsudo nginx -t অংশটি config-এ ভুল আছে কিনা দেখে নেয়। ভুল থাকলে reload হবে না, তাই চলমান site ভাঙে না।
তারপর HTTPS বসাতে দুটি command:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.comCertbot নিজেই 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-এ জিজ্ঞেস করবে:
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-এ কী আছে দেখতে চাইলে:
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