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.

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.
#!/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."# ❌ 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.
// .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"
}# 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.
# 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."// ❌ 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 detailsGestió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.
// 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?
// 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
- Automatiza todo en un solo comando —
./setup.shomake setupdebería llevar a un desarrollador de cero a funcionando en minutos - Haz los scripts idempotentes — ejecutar la configuración dos veces debería producir el mismo resultado que ejecutarla una vez
- Usa Dev Containers para una reproducibilidad total — los entornos basados en Docker eliminan el "en mi máquina funciona" entre sistemas operativos
- Proporciona un Makefile como interfaz universal — comandos consistentes para setup, dev, test y lint independientemente de las herramientas subyacentes
- 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
- Incluye verificación — una prueba de humo al final de la configuración confirma que todo funciona de verdad


