Most JavaScript apps still deploy with SSH and git pull. That breaks the moment you have users. Here are 6 CI/CD patterns that remove downtime, prevent bad deploys, and scale with real traffic.
1. Run Tests Before Every Deploy
If your pipeline deploys without tests, it will ship bugs.
Before (manual deploy):
ssh user@server
cd app
git pull
npm install
pm2 restart app
After (GitHub Actions with test gate):
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: echo "Only runs if tests pass"
Bad code never reaches production. This alone eliminates a large class of outages.
2. Use Environment Variables From Secrets, Not Files
Hardcoding .env works locally and fails in CI.
Before:
DATABASE_URL=postgres://localhost:5432/app
const db = new PrismaClient({
datasources: { db: { url: process.env.DATABASE_URL } }
});
After (GitHub Actions secrets):
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Run migrations
run: npx prisma migrate deploy
Secrets never touch your repo. You remove the risk of leaking credentials and avoid "works locally" failures.
3. Build Once, Deploy Many
Rebuilding on the server creates inconsistencies.
Before:
npm install
npm run build
pm2 restart app
After (build artifact in CI):
- name: Build
run: |
npm ci
npm run build
tar -czf build.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build
path: build.tar.gz
- name: Deploy artifact
run: |
scp build.tar.gz user@server:/app
ssh user@server "cd /app && tar -xzf build.tar.gz"
You deploy the exact same build that passed tests. No drift. No surprises.
4. Zero-Downtime Deploy With PM2 Reload
Restarting the app drops requests.
Before:
pm2 restart app
After:
pm2 reload app --update-env
PM2 spins up new instances before killing old ones. Users never see downtime. For production systems, this is non-negotiable.
5. Separate Migrations From Code Deploy
Schema changes break rollbacks if done wrong.
Before (unsafe):
- run: npx prisma migrate deploy
- run: npm run build && pm2 reload app
After (safe two-step):
-- Migration 1
ALTER TABLE users ADD COLUMN display_name TEXT;
// Deploy 2
return user.display_name || user.name;
Schema first. Code second. This keeps rollback safe because old code still works with the new schema.
This pattern becomes critical when combined with pipelines like the ones in the CI/CD pipeline patterns that prevent broken production deploys where migrations are treated as a separate deployment stage.
6. Rollback in Seconds With Versioned Deploys
If rollback takes minutes, your users already noticed.
Before:
git checkout previous-commit
npm install
npm run build
pm2 restart app
After (Docker versioning):
docker build -t my-app:v15 .
docker tag my-app:v15 my-app:latest
Rollback:
docker stop app
docker run -d --name app my-app:v14
No rebuild. No install. You switch versions instantly.
These patterns are not "DevOps extras." They are baseline expectations for any production system. Start by adding test gates and PM2 reload. Then move to artifacts and versioned deploys. Your deploys go from risky to boring, and boring is exactly what you want in production.
Top comments (0)