Saltar al contenido

Automatización de la configuración de entornos de desarrollo

Cómo pasar de dos días de onboarding a veinte minutos con Dev Containers, scripts de shell e infraestructura como código, en un solo comando.

5 min de lectura
Terminal mostrando un script automatizado de configuración de entorno de desarrollo en ejecución con indicadores de progreso

La peor experiencia de onboarding es un README que dice "instala estas 15 cosas, ejecuta estos 8 comandos, y si te da un error en el paso 6, pregúntale a Dave". Dave dejó la empresa hace tres meses. El README se actualizó por última vez un año antes.

Un entorno de desarrollo reproducible debería ser un solo comando. Clona el repositorio, ejecuta el script de configuración y empieza a escribir código. Todo lo demás es fricción que se acumula con cada nueva incorporación, cada reinstalación del sistema operativo y cada miembro del equipo que pierde un día por la degradación del entorno.

El patrón del script de configuración

El enfoque más simple es un script de shell bien estructurado que automatice lo que de otro modo harías manualmente. Debe ser idempotente: ejecutarlo dos veces no debería romper nada.

shbash
#!/bin/bash
set -euo pipefail
 
# setup.sh — One-command developer environment setup
 
echo "==> Checking prerequisites..."
 
# Check for required system tools
check_command() {
  if ! command -v "$1" &> /dev/null; then
    echo "❌ $1 is not installed. $2"
    exit 1
  fi
  echo "✅ $1 found"
}
 
check_command "node" "Install from https://nodejs.org"
check_command "docker" "Install from https://docker.com"
check_command "git" "Install from https://git-scm.com"
 
# Check minimum versions
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
  echo "❌ Node.js 18+ required, found v$NODE_VERSION"
  exit 1
fi
echo "✅ Node.js v$NODE_VERSION"
 
echo ""
echo "==> Installing dependencies..."
npm ci
 
echo ""
echo "==> Setting up local environment..."
if [ ! -f .env.local ]; then
  cp .env.example .env.local
  echo "✅ Created .env.local from template"
else
  echo "⏭️  .env.local already exists, skipping"
fi
 
echo ""
echo "==> Starting infrastructure..."
docker compose up -d postgres redis
echo "Waiting for PostgreSQL to be ready..."
until docker compose exec -T postgres pg_isready -U app &> /dev/null; do
  sleep 1
done
echo "✅ PostgreSQL is ready"
 
echo ""
echo "==> Running database migrations..."
npm run db:migrate
 
echo ""
echo "==> Seeding development data..."
npm run db:seed
 
echo ""
echo "==> Setup complete! Run 'npm run dev' to start the application."
shbash
# ❌ Manual setup instructions that drift from reality
# README.md:
# 1. Install Node.js 18+
# 2. Install Docker
# 3. Run npm install
# 4. Copy .env.example to .env.local
# 5. Start PostgreSQL: docker run -d ...
# 6. Run migrations: npm run db:migrate
# Note: if migration fails, check that pg_hba.conf allows...
# (nobody reads past step 4)
 
# ✅ Automated setup that IS the documentation
git clone git@github.com:team/project.git
cd project
./setup.sh
# Done. Every step is verified programmatically.

Dev Containers para una reproducibilidad total

Los scripts de shell se encargan de la instalación de herramientas, pero no pueden garantizar versiones idénticas entre sistemas operativos. Los Dev Containers resuelven esto definiendo todo el entorno de desarrollo como una imagen de Docker.

jsonjson
// .devcontainer/devcontainer.json
{
  "name": "Project Dev Environment",
  "dockerComposeFile": "../docker-compose.dev.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
 
  "features": {
    "ghcr.io/devcontainers/features/node:1": {
      "version": "20"
    },
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },
 
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "prisma.prisma"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "typescript.tsdk": "node_modules/typescript/lib"
      }
    }
  },
 
  "postCreateCommand": "npm ci && npm run db:migrate && npm run db:seed",
  "forwardPorts": [3000, 5432, 6379],
 
  "remoteUser": "node"
}
ymlyaml
# docker-compose.dev.yml
services:
  app:
    build:
      context: .
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - .:/workspace:cached
      - node_modules:/workspace/node_modules
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      DATABASE_URL: postgresql://app:devpass@postgres:5432/appdb
      REDIS_URL: redis://redis:6379
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
 
volumes:
  node_modules:
  pgdata:

Makefile como interfaz universal

Un Makefile proporciona una interfaz de comandos consistente independientemente de las herramientas subyacentes. Todos los desarrolladores ejecutan los mismos comandos — make setup, make dev, make test — aunque la implementación cambie.

makefilemakefile
# Makefile — the universal interface to your project
 
.PHONY: setup dev test lint clean db-migrate db-seed help
 
# Default target: show available commands
help: ## Show this help message
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
		sort | awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2}'
 
setup: ## Set up development environment from scratch
	@echo "==> Running setup..."
	@./setup.sh
 
dev: ## Start development server with hot reload
	@docker compose up -d postgres redis
	@npm run dev
 
test: ## Run all tests
	@npm run test
 
test-watch: ## Run tests in watch mode
	@npm run test -- --watch
 
lint: ## Run linter and type checker
	@npm run lint
	@npx tsc --noEmit
 
db-migrate: ## Run database migrations
	@npm run db:migrate
 
db-seed: ## Seed development database
	@npm run db:seed
 
db-reset: ## Reset database (destroy and recreate)
	@docker compose down -v postgres
	@docker compose up -d postgres
	@sleep 3
	@npm run db:migrate
	@npm run db:seed
 
clean: ## Remove build artifacts and dependencies
	@rm -rf node_modules .next dist
	@docker compose down -v
	@echo "Cleaned."
tstypescript
// ❌ Different commands for different developers
// Alice: "I use yarn"
// Bob: "I use pnpm"
// Carol: "I run the database differently on my Mac"
// Result: "works on my machine" syndrome
 
// ✅ Everyone uses the same Makefile interface
// make setup  → Identical environment for everyone
// make dev    → Same dev server command
// make test   → Same test runner
// The Makefile abstracts away implementation details

Gestión de variables de entorno

Los entornos de desarrollo necesitan configuración: claves de API, URLs de base de datos, feature flags. El archivo .env.example documenta cada variable requerida sin exponer valores reales.

tstypescript
// scripts/check-env.ts — Validate environment variables at startup
import { readFileSync, existsSync } from 'fs';
 
interface EnvVar {
  name: string;
  required: boolean;
  default?: string;
  description: string;
}
 
function parseEnvExample(path: string): EnvVar[] {
  const content = readFileSync(path, 'utf-8');
  const vars: EnvVar[] = [];
 
  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
 
    const [name, ...valueParts] = trimmed.split('=');
    const value = valueParts.join('=');
 
    // Comments above the variable describe it
    vars.push({
      name: name.trim(),
      required: !value.includes('optional'),
      default: value || undefined,
      description: '',
    });
  }
 
  return vars;
}
 
function validateEnvironment(): void {
  if (!existsSync('.env.example')) {
    console.warn('No .env.example found — skipping env validation');
    return;
  }
 
  const expected = parseEnvExample('.env.example');
  const missing: string[] = [];
 
  for (const envVar of expected) {
    if (envVar.required && !process.env[envVar.name]) {
      missing.push(envVar.name);
    }
  }
 
  if (missing.length > 0) {
    console.error('Missing required environment variables:');
    for (const name of missing) {
      console.error(`  - ${name}`);
    }
    console.error('\nCopy .env.example to .env.local and fill in the values.');
    process.exit(1);
  }
 
  console.log('✅ All required environment variables are set');
}
 
validateEnvironment();

Verificar que la configuración funciona

El script de configuración debería incluir un paso de verificación que confirme que todo funciona. Ejecuta una prueba rápida de humo: ¿puede la aplicación arrancar, conectarse a la base de datos y responder a un health check?

tstypescript
// scripts/verify-setup.ts
async function verifySetup(): Promise<void> {
  const checks = [
    { name: 'Node modules', check: () => existsSync('node_modules') },
    { name: 'Environment file', check: () => existsSync('.env.local') },
    { name: 'Database connection', check: checkDatabaseConnection },
    { name: 'Redis connection', check: checkRedisConnection },
    { name: 'TypeScript compilation', check: checkTypeScriptCompiles },
  ];
 
  let allPassed = true;
 
  for (const { name, check } of checks) {
    try {
      const result = await check();
      console.log(result ? `✅ ${name}` : `❌ ${name}`);
      if (!result) allPassed = false;
    } catch (error) {
      console.log(`❌ ${name}: ${(error as Error).message}`);
      allPassed = false;
    }
  }
 
  if (!allPassed) {
    console.error('\n⚠️  Some checks failed. Run ./setup.sh to fix.');
    process.exit(1);
  }
 
  console.log('\n🎉 Development environment is ready!');
}

Conclusiones clave

  1. Automatiza todo en un solo comando — ./setup.sh o make setup debería llevar a un desarrollador de cero a funcionando en minutos
  2. Haz los scripts idempotentes — ejecutar la configuración dos veces debería producir el mismo resultado que ejecutarla una vez
  3. Usa Dev Containers para una reproducibilidad total — los entornos basados en Docker eliminan el "en mi máquina funciona" entre sistemas operativos
  4. Proporciona un Makefile como interfaz universal — comandos consistentes para setup, dev, test y lint independientemente de las herramientas subyacentes
  5. Valida las variables de entorno al arrancar — detecta la configuración que falta de inmediato en lugar de fallar más tarde con errores crípticos
  6. Incluye verificación — una prueba de humo al final de la configuración confirma que todo funciona de verdad
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX