DevOps CI/CD for Startups: A Practical Setup Guide
"Move fast and break things" is a startup saying — but in production, breaking things is expensive. A solid CI/CD pipeline lets you move fast and ship reliably. Here's the setup we implement for Softotic clients from day one.
Why CI/CD from Day One?
Every day without CI/CD is a day you're accumulating manual deployment risk. We've seen teams:
- Forget to run tests before deploying
- Ship code that conflicts with a colleague's changes
- Deploy to production without testing on staging first
- Spend hours debugging "works on my machine" issues
A CI/CD pipeline eliminates all of these.
The Stack We Recommend
- GitHub Actions — free for public repos, generous limits for private, native Git integration
- Docker — environment consistency from dev to prod
- Docker Hub / GitHub Container Registry — image registry
- VPS for staging (DigitalOcean, Hetzner) — cheap, controllable
- AWS ECS or a VPS for production
Pipeline Structure
``
develop branch push → [CI] → deploy to staging
main branch push → [CI] → deploy to production
PR opened → [CI] → run tests and lint (no deploy)
`
GitHub Actions: The Complete Workflow
`yaml
.github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
- run: npm run build
deploy-staging:
needs: test
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:staging
- name: Deploy to staging via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.STAGING_HOST }}
username: deploy
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
docker pull ghcr.io/${{ github.repository }}:staging
docker stop app-staging || true
docker run -d --rm --name app-staging \
-p 3000:3000 \
--env-file /home/deploy/.env.staging \
ghcr.io/${{ github.repository }}:staging
deploy-production:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Build and push production image
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
# ... deploy to production
`
Zero-Downtime Deployments
Use Docker with a health check and rolling restart:
`dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1
`
With Nginx as a reverse proxy:
- Start new container on a different port
- Health check passes
- Nginx upstream switches to new container
- Old container stops
Environment Variables
Never commit secrets. Use:
- GitHub Actions Secrets — for CI/CD variables
.env.local on your local machine (git-ignored)
- AWS SSM Parameter Store or Doppler in production
`
.env.example (commit this)
DATABASE_URL=postgresql://user:pass@host:5432/dbname
REDIS_URL=redis://localhost:6379
NEXT_PUBLIC_SITE_URL=https://example.com
`
Monitoring After Deployment
Add post-deploy verification:
`yaml
- name: Smoke test production
run: |
sleep 10
curl -f https://www.example.com/api/health || exit 1
``
Also set up:
- Uptime monitoring: UptimeRobot (free) or Better Uptime
- Error tracking: Sentry
- Logs: Datadog or Papertrail
Conclusion
A CI/CD pipeline is infrastructure, not overhead. Set it up on day one and ship with confidence from the first deployment to the thousandth.
Need DevOps help? Softotic's cloud team sets up production-grade pipelines for startups and enterprises.