Entwicklerumgebungen automatisieren mit Dev Containers und Nix
Schluss mit „works on my machine“: Dev Containers für die IDE-Integration plus Nix für reproduzierbare Dependencies – versioniert, portabel, identisch.

Die Onboarding-Steuer
Jedes neue Teammitglied verbringt Stunden – manchmal Tage – damit, die richtige Node-Version, den richtigen Datenbanktreiber und die richtigen Systemabhängigkeiten zu installieren. README-Anleitungen veralten. Jemand hat Python 3.11, während das Projekt 3.12 braucht. Der CI-Server nutzt eine andere Postgres-Version als die lokale Entwicklung. Das sind lösbare Probleme, und die Lösung sind automatisierte, reproduzierbare Entwicklerumgebungen.
Dev Containers für die IDE-Integration
Dev Containers definieren eine Docker-Umgebung, mit der sich VS Code (oder jede kompatible IDE) direkt verbindet. Die gesamte Entwicklungsumgebung – Runtime, Tools, Erweiterungen, Einstellungen – wird zusammen mit dem Code versioniert.
// .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 für reproduzierbare Abhängigkeiten
Docker bietet Isolation auf Betriebssystemebene. Nix bietet Reproduzierbarkeit auf Paketebene. Jede Abhängigkeit ist über einen content-addressed Store auf eine exakte Version gepinnt – zwei Entwickler mit derselben flake.lock-Datei haben damit byte-identische Toolchains.
# 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 };
}Dev Containers und Nix kombinieren
Der robusteste Ansatz nutzt beides: Dev Containers für die IDE-Erfahrung und die Service-Orchestrierung, Nix innerhalb des Containers für präzise Tool-Versionen. Das gibt dir Dockers Isolation mit Nix' Reproduzierbarkeit.
# .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}`);
}
}Secrets in Entwicklungsumgebungen verwalten
Entwicklungsumgebungen brauchen Zugangsdaten für Datenbanken, APIs und Services. Diese dürfen niemals ins Versionskontrollsystem gelangen – auch nicht in der Entwicklung.
// .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");
}Die wichtigsten Erkenntnisse
Automatisierte Entwicklerumgebungen beseitigen Onboarding-Reibung und „works on my machine“-Bugs. Dev Containers liefern IDE-Integration, Service-Orchestrierung und Erweiterungsmanagement – alles versioniert. Nix liefert Reproduzierbarkeit auf Byte-Ebene für jedes Tool und jede Abhängigkeit und stellt sicher, dass zwei Entwickler nie unterschiedliche Compiler-Versionen haben.
Kombiniere beides für das stärkste Setup: Docker für die Service-Isolation, Nix im Container für Tool-Präzision. Führe ein Umgebungs-Verifikationsskript als Post-Create-Befehl aus, um Konfigurationsdrift sofort zu erkennen. Halte Secrets mit generierten .env-Dateien und Vault-Integration aus der Versionskontrolle heraus.
Der Aufwand sind ein paar Konfigurationsdateien, die zusammen mit dem Code committet werden. Der Gewinn: Jeder Entwickler hat ab dem ersten Tag eine funktionierende Umgebung, die identisch mit CI und mit der jedes anderen Teammitglieds ist – keine Setup-Doku, keine Versionskonflikte, keine verschwendeten Tage.


