Enterprise DevOps advice is written for teams with dedicated platform engineers, multiple environments, and complexity that justifies it. If you're on a 3-person startup or a small agency team, most of it doesn't apply — and trying to implement all of it will slow you down more than it helps.
Here's what actually matters for small teams, prioritized by impact.
Before any code merges, tests must pass. This single practice prevents more production incidents than anything else.
# .github/workflows/ci.yml — minimum viable CI
name: CI
on:
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 test
- run: npm run lintStart here. Even 30% code coverage catching regressions beats 0%.
Docker eliminates "works on my machine" and makes deployment reproducible. One Dockerfile, every environment gets the same image.
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
RUN adduser -S appuser
COPY --from=builder --chown=appuser /app/dist ./dist
COPY --from=builder --chown=appuser /app/node_modules ./node_modules
USER appuser
CMD ["node", "dist/index.js"]If deploying requires SSH-ing into a server and running commands manually, sooner or later someone will skip a step at 5pm on a Friday. Automate it.
GitHub Actions + Docker is the lowest-friction setup for small teams. See the CI/CD with GitHub Actions article on this blog for the full setup.
// BAD
const dbUrl = 'postgresql://admin:password123@prod-db.company.com/appdb'
// GOOD
const dbUrl = process.env.DATABASE_URL
if (!dbUrl) throw new Error('DATABASE_URL environment variable is required')Use .env.example (committed) to document required variables, and .env (gitignored) for actual values. Never commit secrets.
Even a 2-person team benefits from this workflow:
mainmain (production)
└── develop (staging)
├── feature/user-auth
├── feature/email-notifications
└── fix/cart-calculation-bug
Simple branching strategy: feature/*, fix/*, chore/* branches → PR → merge to develop → test on staging → merge to main → auto-deploy.
You need a place to test before production. It doesn't have to be fancy — a separate VPS with the same Docker setup pointing at a test database is sufficient.
The rule: nothing goes to production that hasn't passed on staging first. This catches environment-specific bugs (different OS, different dependency versions, real network conditions).
When something breaks in production, you need logs. console.log to a terminal you can't access doesn't help.
For small teams, the simplest options:
Minimum logging setup:
// lib/logger.ts
const logger = {
info: (msg: string, meta?: object) => {
console.log(JSON.stringify({ level: 'info', msg, ...meta, ts: new Date().toISOString() }))
},
error: (msg: string, error?: unknown, meta?: object) => {
console.error(JSON.stringify({
level: 'error',
msg,
error: error instanceof Error ? { message: error.message, stack: error.stack } : error,
...meta,
ts: new Date().toISOString(),
}))
},
}
export default loggerStructured JSON logs are machine-parseable. Plain text logs require grep and suffer in aggregation tools.
Every service needs a health check:
// Simple but effective
app.get('/health', async (req, res) => {
try {
await db.$queryRaw`SELECT 1` // Verify DB connection
res.json({ status: 'healthy', uptime: process.uptime() })
} catch (error) {
res.status(503).json({ status: 'unhealthy', error: 'Database unreachable' })
}
})Use this in your load balancer, Docker health check, and Kubernetes readiness probe. It prevents routing traffic to a broken instance.
When you have more than 2-3 servers, stop clicking in the AWS console. IaC makes your infrastructure reproducible and reviewable. Start simple:
# main.tf
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.small"
key_name = aws_key_pair.deployer.key_name
tags = {
Name = "app-server"
Environment = "production"
}
}Once you have more than a handful of secrets, a proper vault beats environment variables in CI/CD:
You don't want to find out your app is down when a customer emails you. Set up:
Track deployment frequency — how often you deploy to production. High-performing teams deploy multiple times per day. The practices above make frequent, confident deployment possible.
If deploying is scary, you don't have enough automation. Make it boring.