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:

  1. Connect GitHub repo to Vercel
  2. Vercel detects framework automatically
  3. Every push to main deploys to production
  4. 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:

  1. Connect repository
  2. Set build command and output directory
  3. 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:

  1. Connect GitHub
  2. Railway detects language/framework
  3. Add environment variables
  4. 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

PlatformBest ForFree Tier
VercelFrontend, Next.jsGenerous
NetlifyStatic, JAMstackGenerous
RailwayBackend, databases$5/mo credit
Fly.ioContainers, globalGenerous
RenderFull stackLimited

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:

  1. Triggers on push to main or any PR targeting main
  2. Checks out code
  3. Sets up Node.js with caching
  4. Installs dependencies
  5. 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

TypeWhat It CatchesRun Every Push?
LintingStyle issues, common errorsYes
Type checkingTypeScript errorsYes
Unit testsLogic bugsYes
Integration testsComponent interaction bugsYes
E2E testsFull flow bugsOn 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

  1. Go to repo Settings → Secrets and variables → Actions
  2. Click “New repository secret”
  3. Add name and value
  4. 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:

  1. Tests on every push and PR
  2. Deploys only on pushes to main
  3. Notifies Slack on successful deploy
  4. 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:

  1. Use platform-native deployment (Vercel, Netlify)
  2. Add basic GitHub Actions for testing
  3. Enable preview deployments
  4. Set up notifications

Level up:

  1. Comprehensive test suites
  2. Custom deployment workflows
  3. Database migration automation
  4. 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.