Skip to content
CESAR ZANIS
Tech Reference
Infrastructure

DevOps & Cloud

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.

Need help building CI/CD, reducing cloud waste or using DORA metrics with your team?

Talk to Cesar Zanis