Background
What is CI/CD?
CI (Continuous Integration) automatically checks your code when you push it.
CD (Continuous Deployment) automatically deploys your code to production.
Instead of manually deploying, the system handles everything.
Architecture
My Deployment Setup
- I push code to GitHub
- GitHub Actions runs lint, tests, and build
- If everything passes, a Docker image is built
- The image is deployed to my VPS
- The application restarts automatically
No manual SSH. No risky deployment steps.
Step 01
CI — Automatic Code Validation
Every Pull Request to main runs:
- Install dependencies (pnpm)
- Run ESLint
- Run tests
- Build the Next.js app
If any step fails, the PR cannot be merged.
name: CI
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm buildWhy this matters:
- Lint catches code issues early
- Tests prevent breaking features
- Build ensures production compilation works
Step 02
Dockerizing the App
Instead of running Node directly on the server, I used Docker.
Benefits:
- Same environment everywhere
- Easy deployment
- Clean restarts
- No "works on my machine" problems
# Build Stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm
RUN pnpm install
COPY . .
RUN pnpm build
# Production Stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app ./
EXPOSE 3000
CMD ["pnpm", "start"]Step 03
CD — Automatic Deployment
When code is merged into main, deployment starts automatically.
- Build Docker image
- Push image
- SSH into VPS
- Pull latest image
- Restart container
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker Image
run: docker build -t myapp:latest .
- name: Deploy to Server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
docker pull myapp:latest
docker stop myapp || true
docker rm myapp || true
docker run -d --name myapp -p 3000:3000 myapp:latestSecurity
Managing Secrets
I stored sensitive values inside GitHub Secrets:
- Server IP
- SSH private key
- Docker credentials
- Environment variables
Never commit secrets to Git. Even one leaked SSH key can compromise your server.
Lessons
Mistakes I Faced
- CI was slow due to missing dependency caching
- SSH key permission issues
- Large Docker image size initially
- Environment variable misconfiguration
Each issue helped improve the pipeline.
Result
Final Result
- No manual deployments
- Consistent builds
- Faster releases
- More confidence in production
CI/CD is not optional for production applications. Automation reduces risk and improves reliability.
RJ
Rohith Jayaraj
Full-Stack & DevOps Engineer
