Working CI/CD, Docker used thoughtfully, Kubernetes where it makes sense and FinOps from the start. DORA metrics as a compass, not a presentation trophy.
"In IT, everything is limited by the slowest part. The foundations need to be built as well as possible from the start."
Cesar Zanis
01
DORA metrics: four useful signals
Deployment frequency, lead time, failure rate and recovery time help diagnose delivery performance. Use them to understand bottlenecks, not as isolated targets.
Deployment Frequency
> 1 deployment per day (Elite)vs< 1 deployment per month (Low)
Why it matters
Frequent deployments mean smaller batches and less risk per change. A monthly deployment can accumulate risk rather than reduce it.
How to improve
Reduce batch size. Use feature flags to separate deployment from release.
Lead Time for Changes
< 1 hour (Elite)vs> 6 months (Low)
Why it matters
Time from commit to production. Longer lead times mean slower feedback and later discovery of problems. This is about the learning cycle, not speed for its own sake.
How to improve
Automate pipeline stages. Remove unnecessary manual approvals. Test pull requests before merging.
Change Failure Rate
< 5% (Elite)vs> 46% (Low)
Why it matters
The percentage of deployments that cause an incident. A high rate can indicate weak testing or changes that are too large.
How to improve
Automated tests with meaningful coverage. Blue/green deployment. Rollback in less than five minutes.
MTTR (Mean Time to Restore)
< 1 hour (Elite)vs> 6 months (Low)
Why it matters
How long it takes to restore service after an incident. Recovery speed matters as much as preventing failures.
How to improve
Documented runbooks. Alerts with context, beyond 'high CPU'. Blameless postmortems after every SEV-1 incident.
02
CI/CD: pipelines that work
A minimal working pipeline (GitHub Actions)
YAML
# .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
# Dependency cache: avoids repeated downloads
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test -- --coverage
# Upload coverage for visibility into tests
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
# Build and push the Docker image
- 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 }}:latest
cache-from: type=gha # Cache Docker layers between runs
cache-to: type=gha,mode=max
Multi-stage Dockerfile: the difference between 1.2 GB and 120 MB
YAML
# Multi-stage Dockerfile
# Stage 1: Build (everything needed to compile)
FROM node:20-alpine AS builder
WORKDIR /app
# Copy only package.json first for efficient caching
# This layer is reused when code changes but dependencies do not
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Runtime (only what is needed to run)
FROM node:20-alpine AS runner
WORKDIR /app
# Non-root user: basic hardening
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Copy only the build output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000 NODE_ENV=production
# Health check: exposes container health to the orchestrator
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
# Measure the final image size for your application
03
FinOps: cost as a design concern
A monthly bill should not be the first signal of waste. FinOps connects spending to business value, with visibility and decisions throughout the development cycle.
01
Size for P90 demand, not the maximum peak
Maximum peaks may occur only briefly. Autoscaling handles peaks; right-sizing addresses everyday demand. Measure the actual workload before choosing capacity.
02
Spot/Preemptible instances for stateless workloads
CI/CD, batch processing and queue workers can use discounted interruptible capacity. Design for interruptions and verify the savings for your provider and workload.
03
Reserve instances only for a proven baseline
Reserve capacity you know you will use. In rapidly changing environments, unused commitments can cost more than the discount saves.
04
Cost tags from the start
Without tags by service, team and environment, billing becomes a black box. Define tagging policies before creating resources; retrofitting them is difficult.
05
Budget alerts before an overrun
Set alerts at 50%, 80% and 100% of budget. The early alert gives you time to act before a situation becomes urgent.
Kubernetes: choose it for a real operational need
Kubernetes solves orchestration problems at scale. Before adopting it, consider the number of services, operational complexity and the team's ability to manage the platform.
Use when: multiple services have independent deployment cycles, granular autoscaling is needed, and the team has practical Kubernetes experience.
Avoid when: a working monolith and a small team have no clear orchestration need. Evaluate Docker Compose, Cloud Run or ECS before adding a platform to maintain.