name: Deploy Next.js to VPS
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install & Test
run: |
npm ci
npm run lint
npm run test
- name: Build Standalone
run: npm run build
- name: Deploy to VPS via SSH
uses: appleboy/scp-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: ".next/standalone, .next/static, public"
target: "/var/www/my-app"
- name: Restart PM2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/my-app
cp -r .next/static .next/standalone/.next/
cp -r public .next/standalone/
pm2 reload ecosystem.config.js --update-envAm văzut prea mulți colegi care dau git pull și npm run build direct pe VPS. Pe un server ieftin de 4GB RAM, când Next.js compilează rutele și împachetează Webpack sau Turbopack, OOM Killer-ul de Linux intră peste procese ca mascații și-ți dă jos Nginx-ul sau baza de date.
Am pățit treaba asta anul trecut pe un magazin cu vreo 14k utilizatori activi lunar. Soluția sănătoasă e banală: folosești runnerii gratuiți de la GitHub pentru munca grea (lint, teste, build) și trimiți pe VPS doar rezultatul gata compilat.
De ce facem build-ul în CI și nu pe server
Next.js are prostul obicei să devoreze memorie la build, mai ales dacă folosești generateStaticParams sau pagini SSR masive. Runnerii GitHub au 7GB RAM și resurse dedicate. Dacă crapă ceva la TypeScript sau ESLint, crapă în GitHub Actions, nu în fața clienților tăi.
În plus, am economisit în jur de 3-4 minute la fiecare release. Pe server nu mai instalezi devDependencies, nu mai rulezi TypeScript compiler, nu mai instalezi pachete inutile. Serverul de producție doar primește fișierele și dă un reload la proces.
Structura pipeline-ului: Lint, Test, Build, Ship
Pipeline-ul are două joburi mari. Primul validează codul, al doilea compilează și trimite fișierele prin SSH.
- Etapa de validare: Rulăm
npm run lintși suita dejestsauvitest. Dacă un singur test pică, totul se oprește instant. Nu are sens să pierdem minute compilând ceva stricat. - Etapa de build: Setăm în
next.config.jsopțiuneaoutput: 'standalone'. Asta e cheia. Next.js creează un folder.next/standalonecare conține doar codul minim necesar pentru producție, fără sutele de mega dinnode_modules. - Deploy-ul: Arhivăm folderul standalone, fișierele statice din
publicși folderul.next/static. Le copiem pe VPS prinrsyncsauscp, dezarhivăm și dăm un simplupm2 reload ecosystem.config.js.
Trade-off-uri de care te lovești
Metoda asta e excelentă pentru proiecte medii, dar vine cu niște compromisuri reale.
Merge brici pentru un singur VPS și aplicații monolitice. În schimb, e nasol dacă începi să scalezi orizontal pe 3-4 servere; în punctul ăla, rsync prin SSH devine o ciorbă greu de menținut și ar trebui să treci pe containere Docker împinse într-un registry (GitHub Container Registry) și orchestrate prin Dokku, Coolify sau direct Kubernetes.
Un alt risc stupid: cheile SSH. Dacă folosești un user cu drepturi de root pentru deploy, un secret expus accidental în GitHub îți compromite toată infrastructura. Fă-ți mereu un user dedicat de deploy (ex: deployer), fără privilegii de sudo, care are voie să scrie doar în /var/www/app și să execute doar comanda de reload pentru PM2.
Voi cum împingeți proiectele mici în producție? Mergeți pe varianta clasică cu PM2 și rsync sau ați containerizat deja totul chiar și pe servere simple?