Setting Up CI/CD Pipelines for Web Applications
CI/CD pipelines have transformed how I develop and deploy applications. Before implementing them, deployments were stressful and error-prone. Now, with automated pipelines, I can deploy confidently multiple times a day. Setting up CI/CD properly takes some work, but the benefits are enormous.
What is CI/CD?
CI/CD stands for Continuous Integration and Continuous Deployment/Delivery:
- Continuous Integration (CI): Automatically build and test code changes
- Continuous Deployment/Delivery (CD): Automatically deploy tested code to production
Why CI/CD matters
I've seen teams struggle with manual deployments. CI/CD eliminates:
- Human error in deployments
- Forgotten steps in release processes
- Inconsistent environments
- Slow feedback loops
- Fear of deploying
GitHub Actions basics
GitHub Actions is my go-to for CI/CD. It's built into GitHub and works seamlessly with repositories.
Basic workflow structure:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linting
run: npm run lintTesting strategies
Comprehensive testing is crucial for CI/CD success.
Unit tests:
- name: Run unit tests
run: npm test -- --coverage
env:
CI: trueIntegration tests:
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}End-to-end tests:
- name: Run E2E tests
run: npm run test:e2e
env:
BASE_URL: http://localhost:3000Building and optimization
Automated building ensures consistency.
Next.js build:
- name: Build application run: npm run build - name: Check bundle size run: npx size-limit
Docker builds:
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to registry
run: docker push myregistry.com/myapp:${{ github.sha }}Security scanning
Security should be part of every pipeline.
Dependency scanning:
- name: Run security audit run: npm audit --audit-level=moderate - name: Dependency check uses: dependency-check/Dependency-Check_Action@main
Code quality checks:
- name: CodeQL Analysis
uses: github/codeql-action/init@v2
with:
languages: javascriptDeployment strategies
Different deployment approaches for different scenarios.
Vercel deployment:
- name: Deploy to Vercel
run: npx vercel --prod --yes
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}AWS deployment:
- name: Deploy to AWS
run: |
aws s3 sync ./dist s3://my-bucket --delete
aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} --paths "/*"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Environment management
Different environments for different stages.
Environment-specific secrets:
- name: Deploy to staging
run: npm run deploy:staging
env:
API_URL: ${{ secrets.STAGING_API_URL }}
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
if: github.ref == 'refs/heads/develop'Manual approvals for production:
production:
needs: [test, build]
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
run: npm run deploy:prodBranch protection and PR requirements
Protect your main branches and require quality checks.
Branch protection rules:
# GitHub repository settings # Require PR reviews # Require status checks to pass # Require branches to be up to date # Include administrators
Pull request checks:
name: PR Checks
on:
pull_request:
branches: [ main ]
jobs:
pr-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check PR size
uses: codacy/git-version@2.7.1
with:
release-branch: main
- name: PR labels check
uses: mheap/github-action-required-labels@v2.1.0
with:
mode: exactly
count: 1
labels: "bug, enhancement, feature"Artifact management
Store and version build artifacts.
Upload artifacts:
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
retention-days: 30Download for deployment:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/Caching for performance
Speed up builds with caching.
Node modules caching:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'Docker layer caching:
- name: Docker build with cache
uses: docker/build-push-action@v4
with:
push: false
tags: myapp:cache
cache-from: type=gha
cache-to: type=gha,mode=maxMonitoring and alerting
Keep track of your pipelines.
Pipeline monitoring:
- name: Notify on failure
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Pipeline failed: ${{ github.repository }}"}' \
${{ secrets.SLACK_WEBHOOK_URL }}Performance tracking:
- name: Track build time
run: |
echo "Build completed in ${{ steps.build.outputs.duration }} seconds"Rollback strategies
Be prepared to rollback when things go wrong.
Tagged releases:
- name: Create release
run: |
git tag v${{ github.run_number }}
git push origin v${{ github.run_number }}Automated rollbacks:
- name: Rollback on failure
if: failure()
run: |
# Deploy previous version
git checkout v${{ github.run_number }}-1
npm run deployMulti-environment setups
Manage multiple deployment environments.
Development → Staging → Production:
name: Deploy
on:
push:
branches: [ main, develop ]
jobs:
deploy-dev:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Deploy to development
run: echo "Deploying to dev"
deploy-staging:
needs: deploy-dev
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: echo "Deploying to staging"
deploy-prod:
needs: deploy-staging
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: echo "Deploying to production"Common pitfalls
Avoid these common CI/CD mistakes:
- Too many manual steps: Automate everything possible
- No testing in CI: Always test before deploying
- Ignoring security: Scan for vulnerabilities regularly
- No rollback plan: Always have a way to rollback
- Monolithic pipelines: Break complex pipelines into smaller jobs
Scaling CI/CD
As your team grows, consider these strategies:
Matrix builds: Test across multiple Node.js versions Parallel jobs: Run tests in parallel Self-hosted runners: For better performance Pipeline as code: Store pipeline configuration in version control
Measuring CI/CD success
Track these metrics:
- Deployment frequency: How often you deploy
- Lead time: Time from commit to production
- Failure rate: Percentage of failed deployments
- Recovery time: How quickly you recover from failures
My CI/CD journey
When I first implemented CI/CD, it felt overwhelming. But starting small and iterating has been key. Begin with basic testing and building, then add deployment, then monitoring and security.
CI/CD has transformed how I work. I can ship code confidently, knowing that automated checks will catch issues before they reach users. The initial setup takes time, but the long-term benefits are worth it.
If you're not using CI/CD yet, start today. Even a simple pipeline with testing and linting will improve your development process. As you grow, add more sophisticated checks and automated deployments.
Related articles