DevOps for Startups: CI/CD Without the Overhead
How to set up a production-grade CI/CD pipeline on a startup budget using GitHub Actions, Docker, and a $10/month VPS.
The Startup DevOps Dilemma
Every startup faces the same tension: you need reliable deployments and infrastructure, but you can't afford a dedicated DevOps engineer or expensive cloud services. The good news? Modern tooling has democratized DevOps to the point where a single developer can set up a production-grade pipeline in an afternoon.
This guide walks through the exact setup we use for early-stage startups — battle-tested, budget-friendly, and ready to scale when the time comes.
The Stack
| Component | Tool | Monthly Cost |
|---|---|---|
| CI/CD | GitHub Actions | Free (2,000 min/month) |
| Container Registry | GitHub Container Registry | Free |
| Server | Hetzner VPS (CPX21) | ~$10 |
| Reverse Proxy | Caddy | Free |
| SSL | Caddy (automatic Let's Encrypt) | Free |
| Monitoring | Uptime Kuma + Grafana | Free (self-hosted) |
Total: ~$10/month for a setup that handles 10,000+ daily active users comfortably.
Step 1: Dockerize Your Application
Start with a multi-stage Dockerfile that produces a lean production image:
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
Key principles:
- Multi-stage builds keep the final image small (typically under 200MB)
- Non-root user for security
- Standalone output from Next.js eliminates the need for
node_modulesin production
Step 2: GitHub Actions Workflow
Create a workflow that builds, tests, and deploys on every push to main:
name: Deploy to Production
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
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 run type-check
- run: npm test -- --run
build-and-deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker compose -f /opt/app/docker-compose.prod.yml up -d --force-recreate
Step 3: Server Setup with Caddy
Caddy handles reverse proxying and automatic HTTPS with zero configuration:
# /etc/caddy/Caddyfile
app.yourdomain.com {
reverse_proxy localhost:3000
encode gzip zstd
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
}
That's it. Caddy automatically provisions and renews SSL certificates from Let's Encrypt.
Step 4: Docker Compose for Production
# /opt/app/docker-compose.prod.yml
version: "3.8"
services:
app:
image: ghcr.io/your-org/your-app:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}
- NODE_ENV=production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${DB_NAME}
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
pgdata:
Step 5: Monitoring on a Budget
Self-host Uptime Kuma for uptime monitoring and Grafana with Prometheus for metrics:
- Uptime Kuma sends alerts to Slack/Discord when your site goes down
- Grafana dashboards show CPU, memory, and request metrics
- Both run alongside your app on the same $10 VPS
Zero-Downtime Deployments
For zero-downtime deploys without Kubernetes, use Docker's rolling update strategy:
#!/bin/bash
# deploy.sh - Zero-downtime deployment script
set -e
IMAGE="ghcr.io/your-org/your-app:latest"
# Pull new image
docker pull $IMAGE
# Start new container alongside old one
docker compose -f docker-compose.prod.yml up -d --no-deps --scale app=2 --no-recreate app
# Wait for new container to be healthy
sleep 10
# Remove old container
docker compose -f docker-compose.prod.yml up -d --no-deps --scale app=1 --no-recreate app
When to Graduate
This setup handles significant traffic — we've run applications serving 50,000+ monthly visitors on similar infrastructure. You should consider upgrading when:
- You need horizontal scaling — multiple servers behind a load balancer
- Compliance requirements demand managed services (SOC 2, HIPAA)
- Team grows beyond 5 engineers and you need environment isolation
- Database needs exceed what a single Postgres instance can handle
At that point, move to managed services like Vercel, Railway, or AWS ECS. The containerized setup means migration is straightforward — your Docker images deploy anywhere.
Final Thoughts
DevOps doesn't have to be complex or expensive. A well-structured Docker setup with GitHub Actions gives you:
- Automated testing on every commit
- One-command deployments
- Automatic HTTPS
- Health monitoring
- All for less than the cost of a coffee subscription
Start simple, automate early, and scale when the metrics demand it — not before.
Ready to build something extraordinary?
We help startups and enterprises ship production-grade software powered by AI. Let's discuss your next project.
Get In Touch