CI/CD for Indie Hackers: Ship Faster Without a DevOps Team
Manual deployments are error-prone and slow. You SSH into a server, run some commands, pray nothing breaks, and wonder why you’re afraid to deploy on Fridays.
CI/CD eliminates this. Every push triggers automatic tests. Every merge to main deploys automatically. You ship with confidence, ship often, and spend time building instead of deploying.
This guide covers CI/CD setup for indie hackers and solo developers—practical workflows you can implement today without a DevOps team or years of infrastructure experience.
What CI/CD Actually Means
CI (Continuous Integration): Every code change triggers automated tests. Problems are caught before merging. The codebase stays healthy.
CD (Continuous Delivery): Every successful build is automatically prepared for release. Deploying is a button click away.
CD (Continuous Deployment): Every successful build deploys automatically to production. No manual intervention needed.
Most indie hackers want Continuous Deployment—push to main, see it live in minutes.
The Pipeline
Code Push → Build → Test → Deploy
↓ ↓ ↓ ↓
GitHub Compile Run Ship to
bundle tests production
Each step must pass before the next runs. If tests fail, deployment doesn’t happen.
Why This Matters for Indies
- Ship faster: Deploy in minutes, not hours
- Ship safer: Tests catch problems before users do
- Ship more often: Low-friction deployment means more frequent releases
- Less time on ops: Automation handles the repetitive work
Platform-Native Deployment
The easiest CI/CD uses platform-native deployment. Connect your repo, push code, see it live. No configuration required.
Vercel
Best for: Next.js, React, frontend apps
Setup:
- Connect GitHub repo to Vercel
- Vercel detects framework automatically
- Every push to main deploys to production
- Every pull request gets a preview URL
Zero configuration for:
- Next.js
- React (Create React App, Vite)
- Vue, Svelte, Astro
- Static sites
Features:
- Automatic HTTPS
- Global CDN
- Preview deployments on every PR
- Environment variables management
- Serverless functions
Vercel’s free tier is generous for indie hackers. Most projects never need to pay.
Netlify
Best for: Static sites, JAMstack, serverless
Setup:
- Connect repository
- Set build command and output directory
- Auto-deploys enabled by default
Features:
- Deploy previews for every PR
- Branch deploys (staging branches)
- Forms handling built-in
- Serverless functions
- Edge functions
Example build settings:
Build command: npm run build
Publish directory: dist (or build, out, .next)
Railway
Best for: Backend services, databases, full-stack
Setup:
- Connect GitHub
- Railway detects language/framework
- Add environment variables
- Auto-deploys on push
Features:
- Database provisioning (Postgres, Redis, MongoDB)
- Environment management
- Easy scaling
- Cron jobs
Railway’s $5/month credit covers many indie projects.
Platform Comparison
| Platform | Best For | Free Tier |
|---|---|---|
| Vercel | Frontend, Next.js | Generous |
| Netlify | Static, JAMstack | Generous |
| Railway | Backend, databases | $5/mo credit |
| Fly.io | Containers, global | Generous |
| Render | Full stack | Limited |
For most indie hackers: Vercel or Netlify for frontend, Railway or Fly.io for backend.
GitHub Actions
When platform-native deployment isn’t enough, GitHub Actions provides flexible CI/CD built into GitHub.
Free tier: 2,000 minutes/month for private repos, unlimited for public.
Your First Workflow
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
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 typecheck
- run: npm test
- run: npm run build
This workflow:
- Triggers on push to main or any PR targeting main
- Checks out code
- Sets up Node.js with caching
- Installs dependencies
- Runs lint, typecheck, tests, and build
If any step fails, the workflow fails.
Workflow Anatomy
name: CI # Workflow name (shows in GitHub UI)
on: # When to run
push:
branches: [main]
pull_request:
jobs: # Groups of steps
test:
runs-on: ubuntu-latest # Virtual machine type
steps:
- uses: actions/checkout@v4 # Pre-built action
- run: npm test # Shell command
Common Triggers
on:
push: # On any push
branches: [main] # Only main branch
pull_request: # On PR events
branches: [main]
schedule: # Cron schedule
- cron: '0 0 * * *' # Daily at midnight
workflow_dispatch: # Manual trigger button
Testing in CI
Automated tests are the core value of CI. They catch bugs before deployment.
What to Test
| Type | What It Catches | Run Every Push? |
|---|---|---|
| Linting | Style issues, common errors | Yes |
| Type checking | TypeScript errors | Yes |
| Unit tests | Logic bugs | Yes |
| Integration tests | Component interaction bugs | Yes |
| E2E tests | Full flow bugs | On PRs/deploy |
Basic Test Workflow
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
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Unit tests
run: npm test
- name: Build
run: npm run build
If you don’t have tests yet, at least run linting, type checking, and build verification. These catch many issues.
Deployment Workflows
Deploy to Vercel via GitHub Actions
Vercel handles deployment automatically, but for custom needs:
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Deploy to Netlify
name: Deploy to Netlify
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- uses: nwtgck/actions-netlify@v2
with:
publish-dir: './dist'
production-deploy: true
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
Deploy to Fly.io
name: Deploy to Fly.io
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Environment Variables and Secrets
Never commit secrets to code. Use GitHub’s secrets management.
Adding Secrets
- Go to repo Settings → Secrets and variables → Actions
- Click “New repository secret”
- Add name and value
- Reference in workflow:
${{ secrets.SECRET_NAME }}
Using Secrets in Workflows
jobs:
deploy:
runs-on: ubuntu-latest
env:
NODE_ENV: production
steps:
- run: npm run build
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Environment-Specific Secrets
GitHub supports environments (production, staging, etc.) with different secrets:
jobs:
deploy:
environment: production
runs-on: ubuntu-latest
This uses secrets from the “production” environment.
Preview Deployments
Preview deployments let you test changes before merging.
Automatic Preview URLs
Most platforms provide this automatically:
- Vercel:
your-app-git-branchname.vercel.app - Netlify:
deploy-preview-123--yoursite.netlify.app - Railway: Per-branch environments
Every pull request gets its own URL. Review changes on a real deployment before merging.
Adding Preview URL to PR Comments
- name: Comment Preview URL
uses: actions/github-script@v6
if: github.event_name == 'pull_request'
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Preview deployed to: https://preview-${{ github.event.number }}.example.com'
})
Optimizing CI Time
Faster CI means faster feedback and more productive development.
Caching Dependencies
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Automatically caches node_modules
For other package managers:
cache: 'yarn'
cache: 'pnpm'
Parallel Jobs
Run independent steps in parallel:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
build:
needs: [lint, test] # Only run after lint and test pass
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
Lint and test run simultaneously. Build waits for both to pass.
Cancel Redundant Runs
When you push multiple times quickly, cancel in-progress runs:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Notifications
Know when deploys succeed or fail.
Slack Notifications
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
if: always() # Run even if previous steps fail
Discord Notifications
- name: Notify Discord
uses: sarisia/actions-status-discord@v1
if: failure() # Only on failure
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
Database Migrations
If your deployment includes database changes:
- name: Run migrations
run: npm run db:migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Run migrations before deploying new code. Consider:
- Testing migrations in staging first
- Having a rollback plan
- Making migrations backward-compatible when possible
Complete Example Workflow
A full workflow for a typical Next.js app:
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
- name: Notify on success
uses: 8398a7/action-slack@v3
with:
status: success
fields: repo,message,commit
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
This workflow:
- Tests on every push and PR
- Deploys only on pushes to main
- Notifies Slack on successful deploy
- Cancels redundant runs
CI/CD Checklist
Foundation
- Repository on GitHub
- Basic test suite exists
- Linting configured
- Build command works locally
CI Setup
- Tests run on every push
- Lint checks in CI
- Build verification
- PR checks required before merge
CD Setup
- Auto-deploy on main branch
- Preview deployments on PRs
- Environment variables configured
- Secrets stored securely
Monitoring
- Deploy notifications working
- Failure alerts active
- Build times reasonable (under 5 minutes)
- Rollback plan documented
Key Takeaways
CI/CD isn’t just for big teams. Modern platforms make it accessible to solo developers, and the benefits compound over time.
Start simple:
- Use platform-native deployment (Vercel, Netlify)
- Add basic GitHub Actions for testing
- Enable preview deployments
- Set up notifications
Level up:
- Comprehensive test suites
- Custom deployment workflows
- Database migration automation
- Performance budgets
Every push triggers tests. Every merge deploys automatically. You ship with confidence, ship often, and spend time building instead of deploying.
The developers who ship most often are usually the ones with the best CI/CD. Set it up once, benefit forever.