Automatizando entornos de desarrollo con Dev Containers y Nix
Acaba con el «en mi máquina funciona» combinando Dev Containers para el IDE y Nix para dependencias reproducibles: entornos versionados y portables.

El impuesto de la incorporación
Cada nuevo miembro del equipo pasa horas—a veces días—instalando la versión correcta de Node, el driver de base de datos correcto, las dependencias del sistema correctas. Las instrucciones del README quedan obsoletas. Alguien tiene Python 3.11 mientras el proyecto necesita 3.12. El servidor de CI usa una versión de Postgres distinta a la del desarrollo local. Estos son problemas solucionables, y la solución son entornos de desarrollo automatizados y reproducibles.
Dev Containers para la integración con el IDE
Dev Containers define un entorno Docker al que VS Code (o cualquier IDE compatible) se conecta directamente. Todo el entorno de desarrollo—runtime, herramientas, extensiones, configuración—está versionado junto al código.
// .devcontainer/devcontainer.json
{
"name": "Project Dev Environment",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"prisma.prisma"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
},
"postCreateCommand": "npm ci",
"forwardPorts": [3000, 5432, 6379]
}# .devcontainer/docker-compose.yml
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
command: sleep infinity
postgres:
image: postgres:16
environment:
POSTGRES_DB: app_dev
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:Nix para dependencias reproducibles
Docker te da aislamiento a nivel de sistema operativo. Nix te da reproducibilidad a nivel de paquete. Cada dependencia está fijada a una versión exacta mediante un almacén direccionado por contenido, lo que significa que dos desarrolladores con el mismo archivo flake.lock tienen toolchains idénticos byte a byte.
# flake.nix
{
description = "Project development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodePackages.pnpm
postgresql_16
redis
openssl
pkg-config
];
shellHook = ''
echo "Dev environment loaded"
echo "Node: $(node --version)"
echo "pnpm: $(pnpm --version)"
export DATABASE_URL="postgresql://dev:dev@localhost:5432/app_dev"
'';
};
}
);
}// ❌ README-based setup — drifts constantly
// "Install Node 20, install pnpm, install Postgres 16,
// set these env vars, run these 12 commands..."
// ✅ One command — identical environment every time
// nix develop
// or
// devcontainer up --workspace-folder .
interface EnvironmentSpec {
runtime: { name: string; version: string };
tools: Array<{ name: string; version: string }>;
services: Array<{ name: string; version: string; port: number }>;
envVars: Record<string, string>;
}
function verifyEnvironment(spec: EnvironmentSpec): {
valid: boolean;
mismatches: string[];
} {
const mismatches: string[] = [];
// Check runtime version
const nodeVersion = process.version;
if (!nodeVersion.startsWith(`v${spec.runtime.version}`)) {
mismatches.push(
`Node version mismatch: expected ${spec.runtime.version}, got ${nodeVersion}`
);
}
// Check required env vars
for (const [key, expected] of Object.entries(spec.envVars)) {
if (!process.env[key]) {
mismatches.push(`Missing environment variable: ${key}`);
}
}
return { valid: mismatches.length === 0, mismatches };
}Combinando Dev Containers y Nix
El enfoque más robusto usa ambos: Dev Containers para la experiencia en el IDE y la orquestación de servicios, Nix dentro del contenedor para versiones precisas de herramientas. Esto te da el aislamiento de Docker con la reproducibilidad de Nix.
# .devcontainer/Dockerfile
FROM nixos/nix:latest AS dev
# Enable flakes
RUN echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf
WORKDIR /workspace
# Copy just the Nix files first for caching
COPY flake.nix flake.lock ./
RUN nix develop --command echo "Dependencies cached"
# The dev shell is now pre-built in the image
ENTRYPOINT ["nix", "develop", "--command"]
CMD ["bash"]// scripts/check-env.ts
// Run as postCreateCommand to verify environment
async function checkEnvironment(): Promise<void> {
const checks = [
{ name: "Node.js", command: "node --version", expected: /^v20/ },
{ name: "pnpm", command: "pnpm --version", expected: /^9/ },
{ name: "PostgreSQL", command: "psql --version", expected: /16/ },
];
const results = await Promise.all(
checks.map(async (check) => {
try {
const { stdout } = await exec(check.command);
const matches = check.expected.test(stdout.trim());
return { ...check, stdout: stdout.trim(), pass: matches };
} catch {
return { ...check, stdout: "not found", pass: false };
}
})
);
const failures = results.filter((r) => !r.pass);
if (failures.length > 0) {
console.error("Environment check failed:");
for (const f of failures) {
console.error(` ${f.name}: expected ${f.expected}, got "${f.stdout}"`);
}
process.exit(1);
}
console.log("All environment checks passed");
for (const r of results) {
console.log(` ${r.name}: ${r.stdout}`);
}
}Gestionando secretos en entornos de desarrollo
Los entornos de desarrollo necesitan credenciales para bases de datos, APIs y servicios. Estas nunca deben subirse al control de versiones, ni siquiera en desarrollo.
// .devcontainer/init-secrets.sh generates a .env from a template
// .env.template is committed; .env is gitignored
interface SecretConfig {
name: string;
source: "vault" | "env" | "generated";
generateFn?: () => string;
}
const devSecrets: SecretConfig[] = [
{
name: "DATABASE_URL",
source: "generated",
generateFn: () =>
"postgresql://dev:dev@postgres:5432/app_dev",
},
{
name: "REDIS_URL",
source: "generated",
generateFn: () => "redis://redis:6379",
},
{
name: "JWT_SECRET",
source: "generated",
generateFn: () => {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Buffer.from(bytes).toString("base64");
},
},
{
name: "EXTERNAL_API_KEY",
source: "vault", // Fetched from secrets manager
},
];
function generateEnvFile(secrets: SecretConfig[]): string {
return secrets
.map((s) => {
if (s.source === "generated" && s.generateFn) {
return `${s.name}=${s.generateFn()}`;
}
return `${s.name}=# Set manually or fetch from vault`;
})
.join("\n");
}Conclusiones clave
Los entornos de desarrollo automatizados eliminan la fricción de la incorporación y los bugs del «en mi máquina funciona». Dev Containers proporciona integración con el IDE, orquestación de servicios y gestión de extensiones—todo versionado. Nix proporciona reproducibilidad a nivel de byte para cada herramienta y dependencia, garantizando que dos desarrolladores nunca tengan versiones de compilador distintas.
Combina ambos para la configuración más sólida: Docker para el aislamiento de servicios, Nix dentro del contenedor para la precisión de herramientas. Ejecuta un script de verificación del entorno como comando post-creación para detectar la deriva de configuración de inmediato. Mantén los secretos fuera del control de versiones con archivos .env generados e integración con un vault.
La inversión son unos pocos archivos de configuración subidos junto al código. El retorno es que cada desarrollador, desde el primer día, tiene un entorno funcional idéntico al de CI y al de cualquier otro miembro del equipo—sin documentación de instalación, sin discrepancias de versiones, sin días perdidos.


