config.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
export const env = envSchema.parse(process.env);
### Step 2: Implement Deterministic CI
GitHub Actions provides native repository integration, zero infrastructure overhead, and predictable execution. Configure caching, matrix testing, and explicit dependency resolution to eliminate flaky runs.
```yaml
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: test_db
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports: ['6379:6379']
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
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 test:coverage
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
REDIS_URL: redis://localhost:6379
Step 3: Enforce Quality Gates
Automated checks must block merges that violate contracts. Combine static analysis, dependency auditing, and coverage thresholds.
# .github/workflows/quality.yml
name: Quality Gates
on: pull_request
jobs:
gates:
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 audit --audit-level=high
- run: npx tsc --noEmit
- run: npm run test:coverage -- --coverageThreshold='{"global":{"lines":80,"functions":80}}'
Step 4: Infrastructure as Code & Zero-Downtime Deploys
Manual server provisioning creates configuration drift. Use Terraform or Pulumi to declare infrastructure. Pair with containerization and rolling deployments to eliminate downtime.
# main.tf (Terraform)
resource "aws_ecs_task_definition" "app" {
family = "solo-app"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
container_definitions = jsonencode([{
name = "app"
image = "${var.ecr_repo}:latest"
portMappings = [{ containerPort = 3000, protocol = "tcp" }]
environment = [
{ name = "DATABASE_URL", value = var.db_url }
]
}])
}
Deploy via GitHub Actions using ECS rolling updates or Railway/Fly.io for simplified solo infrastructure:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
- uses: docker/build-push-action@v5
with: { push: true, tags: ghcr.io/${{ github.repository }}:${{ github.sha }} }
- run: |
flyctl deploy --image ghcr.io/${{ github.repository }}:${{ github.sha }} --strategy rolling
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Step 5: Observability & Self-Healing
Solo developers cannot afford to chase silent failures. Implement structured logging, health endpoints, and automated alerting tied to SLOs.
// health.ts
import { Router } from 'express';
import { db, redis } from './clients';
const router = Router();
router.get('/health', async (req, res) => {
const checks = {
db: await db.ping().catch(() => false),
redis: await redis.ping().catch(() => false),
};
const status = Object.values(checks).every(Boolean) ? 'ok' : 'degraded';
res.status(status === 'ok' ? 200 : 503).json({ status, checks });
});
export default router;
Route /health through your load balancer or platform health check. Configure alerts on 5xx rates, latency p95, and dependency failures. Use Webhook.site or Slack incoming webhooks for lightweight solo alerting.
Architecture Decisions
- GitHub Actions over self-hosted CI: Eliminates maintenance overhead, provides native secrets management, and scales automatically.
- Containerization over bare-metal: Guarantees environment parity between local, CI, and production.
- Declarative IaC over manual consoles: Enables version-controlled infrastructure, reproducible environments, and rapid disaster recovery.
- Rolling deploys over blue-green for solo: Blue-green requires double infrastructure cost. Rolling updates balance safety and budget.
- Structured logs over console.log: Enables automated parsing, metric extraction, and alerting without manual log diving.
Pitfall Guide
-
Over-automating before product-market fit
Building complex pipelines for an unvalidated product wastes cycles. Automate only the path to deployment; defer advanced features like canary releases or multi-region failover until scale demands them.
-
Ignoring secret rotation and audit trails
Static credentials compound risk. Use short-lived tokens, enable secret scanning, and rotate keys quarterly. Treat secrets as code: versioned, encrypted, and access-controlled.
-
Pipeline brittleness from implicit dependencies
Hardcoded paths, implicit cache keys, and non-deterministic package resolution cause false failures. Pin versions, use lockfiles, and validate caches explicitly (npm ci over npm install).
-
Lack of automated rollback strategy
Deploying without a one-click rollback turns incidents into prolonged outages. Tag releases, store previous images, and implement a rollback.yml workflow that redeploys the last known good SHA.
-
Monitoring without alerting thresholds
Dashboards without triggers create false security. Define SLOs (e.g., p95 < 500ms, error rate < 0.1%). Configure alerts that page only when thresholds breach. Silence noisy metrics.
-
Treating automation as "set and forget"
Pipelines decay. Dependencies update, platforms change, and edge cases emerge. Schedule monthly pipeline reviews, update base images, and prune unused workflows.
-
Copy-pasting enterprise CI configurations
Enterprise pipelines assume dedicated SREs, multi-stage approvals, and redundant infrastructure. Strip them down to solo essentials: build, test, deploy, verify. Remove matrix expansions, parallel stages, and approval gates unless justified.
Production Bundle
Action Checklist
Decision Matrix
| Platform | Cost (Solo) | Learning Curve | Solo-Friendly | Ecosystem | Rollback Support |
|---|
| GitHub Actions | Free (2k min/mo) | Low | High | Native | Native (SHA tagging) |
| GitLab CI | Free (400 min/mo) | Medium | Medium | Strong | Manual/Scripted |
| CircleCI | Free (2.5k min/mo) | Medium | Low | Enterprise | Manual |
| Fly.io + Actions | Pay-as-you-go | Low | High | Platform-native | One-command rollback |
| Custom Scripts | Free | High | Low | Fragmented | Error-prone |
GitHub Actions + platform CLI (Fly/Railway) delivers the optimal balance of cost, simplicity, and reliability for solo workflows.
Configuration Template
Copy-paste ready pipeline with pre-commit, CI, and deploy stages.
# .github/workflows/solo-ci-cd.yml
name: Solo CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
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: npx tsc --noEmit
- run: npm run test:coverage -- --coverageThreshold='{"global":{"lines":80}}'
deploy:
needs: validate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
- uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- run: flyctl deploy --image ghcr.io/${{ github.repository }}:${{ github.sha }} --strategy rolling
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100
}
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
hooks:
- id: prettier
args: [--write]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.48.0
hooks:
- id: eslint
args: [--fix, --ext, .ts,.tsx]
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
Quick Start Guide
- Initialize repository structure: Create
.github/workflows/, Dockerfile, .pre-commit-config.yaml, and docker-compose.yml (for local services). Run pre-commit install.
- Validate locally: Execute
npm ci && npm run lint && npm run test:coverage. Confirm pre-commit hooks block invalid commits.
- Trigger pipeline: Push to
main. Verify CI runs validate job, then deploy job builds and pushes the image.
- Verify production: Access
/health endpoint. Confirm status ok. Test rollback by manually deploying previous SHA via platform CLI.
- Schedule maintenance: Enable Dependabot/Renovate. Configure weekly pipeline review reminder. Document rollback steps in
README.md.
Automation for solo developers is not about replicating enterprise complexity. It is about eliminating manual friction, preserving focus, and ensuring that every commit moves the product forward without operational debt. Implement deterministically, monitor conservatively, and iterate only when scale demands it.