# cPanel-এ Next.js deploy: GitHub Actions + FTP দিয়ে সম্পূর্ণ CI/CD

---

অনেক বাংলাদেশি hosting company এখন cPanel-এ **"Setup Node.js App"** দেয়। এর মানে শুধু static HTML না, পুরো Next.js app (SSR, API route, rewrite সহ) shared hosting-এই চালানো যায়। কিন্তু প্রতিবার হাতে build করে FileZilla দিয়ে শত শত file পাঠানো ঝামেলার, আর FTP-তে মাঝেমধ্যে file fail-ও হয়।

এই লেখায় দেখাব কীভাবে `main` **branch-এ push করলেই** GitHub নিজে build করে, শুধু বদলানো file FTP দিয়ে পাঠায়, আর app restart করে।

## কেন GitHub-এ build, server-এ না?

Next.js build-এ ১-২ GB RAM লাগে। Shared hosting সাধারণত এর চেয়ে কম দেয়, তাই server-এ build প্রায়ই মাঝপথে বন্ধ হয়ে যায়। GitHub Actions-এর runner-এ যথেষ্ট RAM আছে, আর খরচও লাগে না (public/private দুই repo-তেই মাসে নির্দিষ্ট পরিমাণ মিনিট free)।

Server-এ তখন শুধু দরকার:

- `.next`: build output
- `public`
- `server.js`
- `next.config.mjs`
- `package.json` আর `package-lock.json`

`app`, `components`, `lib`\-এর মতো source folder লাগে না। `next build` এগুলো compile করে `.next`\-এ ঢুকিয়ে দেয়।

## ধাপ ১: `server.js` (cPanel-এর startup file)

cPanel-এর Node.js App চলে Passenger দিয়ে, যার একটা startup file লাগে। Project-এর root-এ রাখুন:

```js
// server.js
const { createServer } = require("node:http");
const next = require("next");

const port = Number(process.env.PORT) || 3000;
const app = next({ dev: false, dir: __dirname });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  createServer((req, res) => handle(req, res)).listen(port, () => {
    console.log(`Ready on port ${port}`);
  });
});
```

> ⚠️ `next.config`\-এ `output: "export"` **দেবেন না।** ওটা static export, তখন rewrite, redirect, `[id]`\-এর মতো dynamic route আর API proxy কাজ করে না।

## ধাপ ২: cPanel-এ Node.js App বানান

cPanel → **Setup Node.js App → Create Application**:

| ঘর | মান |
| --- | --- |
| Node.js version | ২০ বা তার বেশি (CI-তেও একই major version রাখুন) |
| Application mode | Production |
| Application root | `myapp-web` |
| Application URL | আপনার domain |
| Application startup file | `server.js` |

Create চাপলে cPanel domain-এর document root-এ (সাধারণত `public_html`) একটা `.htaccess` বানায়, যার ভেতরে `PassengerAppRoot` … লেখা থাকে। **এই file মুছবেন না।** এটাই domain-এর request আপনার app-এ পাঠায়।

প্রথমবার হাতে একবার file upload করে **Run NPM Install** চাপুন, তারপর **Start App**। `node_modules` cPanel নিজেই manage করে (একটা symlink হিসেবে)।

## ধাপ ৩: শুধু deploy-এর জন্য আলাদা FTP account

Main cPanel password CI-তে দেবেন না। **cPanel → FTP Accounts**:

- Login: `deploy`
- Directory: `myapp-web` (app root)

এই account শুধু ওই folder দেখতে পায়। তাই FTP-তে তার কাছে app root-ই হলো `/`।

## ধাপ ৪: GitHub Secrets

Repo → **Settings → Secrets and variables → Actions → Secrets** tab → **New repository secret**:

| Secret | উদাহরণ |
| --- | --- |
| `FTP_SERVER` | `ftp.example.com` |
| `FTP_USERNAME` | `deploy@example.com` |
| `FTP_PASSWORD` | deploy account-এর password |
| `FTP_SERVER_DIR` | `/` |

`FTP_SERVER_DIR`\-এ server-এর পুরো path (`/home/user/myapp-web`) **দেবেন না**। FTP account-এর দৃষ্টিতে path দিতে হয়।

## ধাপ ৫: Workflow

`.github/workflows/deploy-web.yml`:

```yaml
name: Deploy website (cPanel)

on:
  push:
    branches: [main]
  workflow_dispatch:

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

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24        # cPanel app-এর সাথে মিলিয়ে
          cache: npm

      - run: npm ci

      - name: Build
        env:
          # build-এর সময়ই বসে যায় — এখানে production মান দিন
          API_PROXY_TARGET: https://api.example.com
        run: npm run build

      - name: Assemble upload folder
        run: |
          mkdir -p deploy-web/tmp
          cp -r .next public server.js next.config.mjs package.json package-lock.json deploy-web/
          rm -rf deploy-web/.next/cache
          # এই file বদলালে Passenger app restart করে
          date -u > deploy-web/tmp/restart.txt

      - name: Upload over FTP
        uses: SamKirkland/FTP-Deploy-Action@v4.3.5
        with:
          server: ${{ secrets.FTP_SERVER }}
          username: ${{ secrets.FTP_USERNAME }}
          password: ${{ secrets.FTP_PASSWORD }}
          local-dir: ./deploy-web/
          server-dir: ${{ secrets.FTP_SERVER_DIR }}
          exclude: |
            **/.git*
            **/.git*/**
            node_modules/**
```

**এর কয়েকটা সুবিধা:**

- **FTP-Deploy-Action শুধু বদলানো file পাঠায়।** Server-এ একটা state file রেখে আগের deploy-এর সাথে মিলিয়ে নেয়। তাই দ্বিতীয়বার থেকে upload খুব দ্রুত হয়, আর পুরনো `.next` chunk-গুলো নিজে থেকেই মুছে যায়।
- `tmp/restart.txt`**:** Passenger এই file-এর সময় বদলেছে দেখলে app restart করে। তাই cPanel-এ ঢুকে Restart চাপতে হয় না।
- `concurrency`**:** দুটো push পরপর এলে দুটো deploy একসাথে চলে একে অপরের file গোলমাল করে না।

## যা হাতে করতে হয়

- **নতুন package:** `package.json`\-এ নতুন package যোগ হলে deploy-এর পরে cPanel-এ একবার **Run NPM Install** চাপুন। SSH থাকলে এটাও workflow-তে যোগ করা যায়।
- **Rollback:** স্বয়ংক্রিয় rollback নেই। ভুল deploy হলে আগের commit-এ ফিরে **Run workflow** চালান।

## যেসব সমস্যায় আমরা পড়েছিলাম (আর সমাধান)

**১.** `Error: Input required and not supplied: server` Workflow secret খুঁজে পায়নি। দেখুন secret **Repository secrets**\-এ আছে কিনা (Variables বা Environment secrets-এ না), নাম হুবহু মিলছে কিনা, আর ঠিক repo-তে বানানো কিনা। Secret যোগ করার পরে run-টা **Re-run** করুন।

**২. Site খুললে LiteSpeed-এর "404 – The resource requested could not be found"** Request app পর্যন্ত পৌঁছাচ্ছে না। দুটো জিনিস দেখুন:

- Domain-এর document root-এ Passenger-এর `.htaccess` আছে কিনা। মুছে গেলে Setup Node.js App-এ app খুলে **Save** আর **Restart** চাপুন, cPanel আবার বানিয়ে দেয়।
- Domains page-এ document root আসলে কোন folder।

**৩. Production-এ API-র ঠিকানা** `localhost` **হয়ে গেছে** `next.config`\-এর rewrite আর `NEXT_PUBLIC_*` মান **build-এর সময়** বসে যায়। আর `.env.local` সবসময় `.env.production`\-এর উপরে প্রাধান্য পায়। তাই local-এ build করলে আপনার dev-এর `.env.local`\-এর localhost-ই production-এ চলে যেতে পারে। CI-তে `env:` দিয়ে মান দেওয়াই সবচেয়ে নিরাপদ। চাইলে `next.config`\-কে phase দেখে সিদ্ধান্ত নিতে দিন:

```js
import { PHASE_DEVELOPMENT_SERVER } from "next/constants.js";

export default (phase) => {
  const target =
    process.env.API_PROXY_TARGET ??
    (phase === PHASE_DEVELOPMENT_SERVER ? "http://localhost:5000" : "https://api.example.com");
  return {
    async rewrites() {
      return [{ source: "/api/:path*", destination: `${target}/api/:path*` }];
    },
  };
};
```

**৪. Windows-এ বানানো zip cPanel-এ extract করলে folder না হয়ে এলোমেলো file** PowerShell-এর `Compress-Archive` zip-এর ভেতরে `\` দিয়ে path লেখে, আর Linux সেটাকে file-এর নামের অংশ ধরে। হাতে zip বানাতে হলে Windows-এর `tar.exe -a -c -f site.zip -C folder .` ব্যবহার করুন। CI-তে এই সমস্যা হয়ই না, কারণ সেখানে zip লাগে না।

**৫.** `stderr.log`**\-এ** `getaddrinfo ENOTFOUND api.example.com` App ঠিকই চলছে, শুধু API-র domain এখনো তৈরি হয়নি বা DNS ছড়ায়নি। Website দেখানোয় এর কোনো প্রভাব নেই।

## শেষ কথা

| ধাপ | একবার |
| --- | --- |
| `server.js` | ✅ |
| cPanel Node.js App + প্রথম NPM Install | ✅ |
| শুধু app folder-এর জন্য FTP account | ✅ |
| ৪টা GitHub secret + workflow | ✅ |

এরপর থেকে শুধু `git push`। কয়েক মিনিটের মধ্যে নতুন version live হয়ে যায়, cPanel-এ ঢোকারও দরকার পড়ে না।

---

চাইলে এটা একটা শেয়ার করার মতো web page বা doc হিসেবেও বানিয়ে দিতে পারি।
