<!-- Canonical: https://lucioduran.com/es/blog/nix-flakes-reproducible-dev-environments
Language: es
Content-SHA256: 51b750c0952d4b467d4e7716bc82f29fe0ae2b4d294d10087dbc9efea19bb0b1
-->

DevOps

# Nix Flakes como Reemplazo de Docker para Entornos de Desarrollo Local

2025-11-199 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/nix-flakes-reproducible-dev-environments>)

## Nix Flakes: Visión Funcional

Los [Nix Flakes](<https://nixos.wiki/wiki/Flakes>) suelen describirse con terminología abstracta que oscurece su valor práctico. Un enfoque más concreto es el siguiente.

Un Nix Flake es un archivo ` flake.nix ` en tu repo que declara, con cero ambigüedad:

1. **Exactamente** qué versiones de cada herramienta necesita tu proyecto
2. **Exactamente** cómo compilar tu proyecto
3. **Exactamente** cómo se ve el entorno de desarrollo

Y "exactamente" se refiere a pineado-por-hash-a-un-commit-específico-de-nixpkgs exactamente. No "Node 20.x". No "lo que te dé Homebrew." El build específico de Node.js, compilado con flags específicos, linkeado contra librerías específicas.

Acá está el ` flake.nix ` de un servicio de producción representativo, una API en Go con dependencia de PostgreSQL y algunas herramientas de generación de código:

```nix
{
 description = "Order service API";

 inputs = {
 nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
 flake-utils.url = "github:numtide/flake-utils";
 gomod2nix = {
 url = "github:nix-community/gomod2nix";
 inputs.nixpkgs.follows = "nixpkgs";
 };
 };

 outputs = { self, nixpkgs, flake-utils, gomod2nix }:
 flake-utils.lib.eachDefaultSystem (system:
 let
 pkgs = import nixpkgs {
 inherit system;
 overlays = [ gomod2nix.overlays.default ];
 };
 in {
 devShells.default = pkgs.mkShell {
 buildInputs = with pkgs; [
 go_1_23
 gopls
 gotools
 go-mockery
 sqlc
 postgresql_16
 pgformatter
 protobuf
 protoc-gen-go
 protoc-gen-go-grpc
 buf
 just
 docker-compose
 ];

 shellHook = ''
 export GOPATH="$PWD/.go"
 export PATH="$GOPATH/bin:$PATH"
 export PGDATA="$PWD/.pgdata"
 export PGHOST="$PWD/.pgdata"
 echo "Order service dev shell listo"
 echo "Go $(go version | cut -d' ' -f3)"
 echo "PostgreSQL $(pg_config --version)"
 '';
 };

 packages.default = pkgs.buildGoApplication {
 pname = "order-service";
 version = "0.1.0";
 src = ./.;
 modules = ./gomod2nix.toml;
 };
 });
}
```

Cuando alguien ejecuta ` nix develop `, obtiene un shell con cada herramienta en la versión exacta especificada. Go 1.23. PostgreSQL 16. protoc, buf, sqlc, todo. En Linux, en macOS, en Intel, en ARM. Las mismas versiones. Siempre.

## La Anatomía de flake.nix (Las Partes Que Importan)

A continuación se desglosa la estructura, ya que la documentación de Nix lo hace más difícil de lo necesario:

### inputs

```nix
inputs = {
 nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
 flake-utils.url = "github:numtide/flake-utils";
};
```

Estas son tus dependencias. ` nixpkgs ` es el repositorio de paquetes (más de 100,000 paquetes). La URL pinea a un branch específico. Cuando ejecutás ` nix flake update `, actualiza ` flake.lock ` con el hash exacto del commit, como un ` package-lock.json ` pero para todo el toolchain de tu sistema.

### outputs

Acá es donde se define lo que tu flake provee. Los outputs más comunes para un proyecto de desarrollo:

- ` devShells.default `, el entorno de desarrollo (lo que usa ` nix develop `)
- ` packages.default `, el artefacto construido (lo que usa ` nix build `)
- ` checks `, suites de tests (lo que ejecuta ` nix flake check `)

### devShells (La Feature que Vale Oro)

```nix
devShells.default = pkgs.mkShell {
 buildInputs = [ ... ]; # Herramientas disponibles en el shell
 shellHook = ''...''; # Se ejecuta al entrar al shell
 env = { ... }; # Variables de entorno
};
```

` mkShell ` crea un entorno, no un container. No hay overhead de aislamiento, no hay montaje de volúmenes, no hay bridge de red. Tu editor, tu terminal, tu config de git, todo funciona normal. Simplemente teners herramientas diferentes en tu PATH.

## Por Qué Le Gana a Docker para Desarrollo Local

La precisión importa aquí porque es donde la gente se pone a la defensiva. Docker es excelente para deployment. Nix no está reemplazando tus containers de producción. Lo que Nix reemplaza es el ` Dockerfile.dev ` y el ` docker-compose.dev.yml ` que manteners separados de tu setup de producción.

**Velocidad**: ` nix develop ` con cache caliente toma 0.3 segundos. Levantar un Docker Compose de dev con 4 servicios: 12-20 segundos. A lo largo de un día de trabajo, eso se suma.

**Uso de recursos**: Un devShell de Nix usa cero RAM adicional más allá de las herramientas en sí. Docker Desktop en macOS reserva una VM con 4-8GB de RAM por defecto.

**Performance del file system**: Este es el factor crítico en macOS. El compartido de file system de Docker (incluso con VirtioFS) agrega latencia medible a cada operación de archivo. Para un ` go test ./... ` en un servicio típico: 4.1s nativo, 7.8s en Docker. Para ` npm install ` en un proyecto mediano: 12s nativo, 34s en Docker.

**Debugging**: Cuando algo en tu shell de Nix no funciona, es posible inspeccionarlo. ` which go ` te muestra el path exacto del Nix store. ` nix why-depends ` traza la cadena de dependencias. En Docker, se está haciendo SSH a un container y adivinando.

### Una Comparación Real

Checklist de migración, lo que se reemplaza:

| Basado en Docker | Basado en Nix |
| --- | --- |
| ` Dockerfile.dev ` (47 líneas) | ` flake.nix ` (43 líneas) |
| ` docker-compose.dev.yml ` (89 líneas) | , (servicios corren nativamente) |
| ` .env.docker ` | , (shellHook setea env vars) |
| ` scripts/setup-dev.sh ` (112 líneas) | , |
| ` scripts/fix-permissions.sh ` | , |
| ` docs/SETUP.md ` (47 pasos) | ` docs/SETUP.md ` (3 pasos) |

Los tres pasos: 1) Instalar Nix, 2) Ejecutá ` nix develop `, 3) Ejecutá ` just dev `.

## Cross-Compilación: El Arma Secreta de Nix

Esta es una feature genuinamente sorprendente. La historia de cross-compilación de Nix es absurdamente buena:

```nix
packages = {
 default = pkgs.buildGoApplication {
 pname = "order-service";
 version = "0.1.0";
 src = ./.;
 modules = ./gomod2nix.toml;
 };

 # Cross-compilar para ARM64 Linux (para deploy en Graviton)
 arm64-linux = pkgs.pkgsCross.aarch64-multiplatform.buildGoApplication {
 pname = "order-service";
 version = "0.1.0";
 src = ./.;
 modules = ./gomod2nix.toml;
 };

 # Construir una imagen Docker sin Docker
 dockerImage = pkgs.dockerTools.buildLayeredImage {
 name = "order-service";
 tag = "latest";
 contents = [ self.packages.${system}.default ];
 config = {
 Cmd = [ "/bin/order-service" ];
 ExposedPorts = { "8080/tcp" = {}; };
 };
 };
};
```

Ese output ` dockerImage ` es hermoso. Construye una imagen de container OCI-compliant sin necesitar Docker instalado. La imagen resultante típicamente pesa 10-30MB porque solo incluye exactamente lo que la aplicación necesita, sin bloat de imagen base, sin herramientas de build sobrantes.

Las imágenes ARM64 Linux se pueden construir en máquinas de desarrollo macOS para testing. Sin QEMU, sin emulación. Nix simplemente cross-compila.

## Migración: Un Relato Honesto

Este proceso no siempre es suave. Acá va una línea de tiempo representativa de migrar un equipo de 6:

**Semana 1**: Escribir el ` flake.nix ` y hacerlo funcionar en una máquina. Esto toma unos 3 días por la pelea con el lenguaje Nix. Los mensajes de error son... no son geniales. ` error: attribute 'go_1_23' missing ` cuando en realidad tenía un typo en el overlay. Qué útil.

**Semana 2**: Dos senior desarrolladores instalan Nix y prueban el flake. Problema inmediato: la VPN corporativa de un dev bloquea el binary cache de Nix (` cache.nixos.org `). Levantar un cache interno en S3 usando Attic toma un día entero.

**Semana 3**: Lanzamiento a todo el equipo. Tres problemas típicos:

1. Un dev en una Mac Intel vieja tenía un string de arquitectura diferente. Se arregló asegurando que ` flake-utils.lib.eachDefaultSystem ` se usara (ya se usaba).
2. Nuestros pre-commit hooks asumían herramientas instaladas globalmente. Se arregló ejecutándolos dentro de ` nix develop `.
3. La extensión de Go de VS Code no encontraba ` gopls ` hasta que agregamos la integración con ` direnv `.

**Semana 4**: Todos en Nix. El setup de Docker para desarrollo se borra. Sin quejas.

## Integración con direnv: La Pieza Faltante

` nix develop ` a secas requiere que entres manualmente al shell cada vez que abrís una terminal. ` direnv ` con ` nix-direnv ` automatiza esto:

```bash
# .envrc
use flake
```

Ese es el archivo completo. Al ejecutar ` cd ` en el directorio del proyecto, direnv automáticamente carga el devShell de Nix. Tu editor levanta las herramientas. Tu terminal tiene el PATH correcto. Es invisible una vez configurado.

Instalalo una vez:

```bash
# En tu config global de Nix o home-manager
nix profile install nixpkgs#direnv nixpkgs#nix-direnv
```

Agregar a tu shell rc:

```bash
eval "$(direnv hook bash)" # o zsh
```

## Múltiples devShells para Diferentes Flujos de Trabajo

Un ` flake.nix ` puede proveer múltiples shells:

```nix
devShells = {
 default = pkgs.mkShell {
 buildInputs = with pkgs; [ go_1_23 gopls postgresql_16 ];
 };

 ci = pkgs.mkShell {
 buildInputs = with pkgs; [ go_1_23 postgresql_16 golangci-lint ];
 };

 docs = pkgs.mkShell {
 buildInputs = with pkgs; [ mdbook graphviz plantuml ];
 };
};
```

` nix develop ` entra al shell default. ` nix develop .#ci ` entra al shell de CI. ` nix develop .#docs ` entra al shell de documentación. Cada uno es hermético y reproducible.

## El Binary Cache: Por Qué los Builds Siguientes Son Rápidos

El primer ` nix develop ` puede ser lento, minutos si los paquetes necesitan compilarse desde source. Pero Nix tiene un binary cache (` cache.nixos.org `) que provee binarios pre-compilados para la mayoría de los paquetes. Después de tu primera ejecución, las llamadas subsiguientes a ` nix develop ` son casi instantáneas porque todo está cacheado en el Nix store local.

Para equipos, se recomienda fuertemente levantar un cache compartido. Attic sobre un bucket de S3 funciona bien:

```nix
# En tu nix.conf o nixConfig del flake
extra-substituters = https://tu-cache-equipo.s3.amazonaws.com
extra-trusted-public-keys = tu-cache-equipo:AAAA...==
```

Los builds de CI llenan el cache. Las máquinas de desarrollo consumen de él. Un ` nix develop ` completo con cache caliente toma menos de un segundo.

## Flake Checks: El Mejor Amigo de Tu CI

```nix
checks.${system} = {
 build = self.packages.${system}.default;

 lint = pkgs.runCommand "lint" {
 buildInputs = [ pkgs.golangci-lint ];
 } ''
 cd ${self}
 golangci-lint run ./...
 touch $out
 '';

 test = pkgs.runCommand "test" {
 buildInputs = [ pkgs.go_1_23 pkgs.postgresql_16 ];
 } ''
 cd ${self}
 go test -race -count=1 ./...
 touch $out
 '';
};
```

` nix flake check ` ejecuta todos los checks. En CI, esto garantiza las mismas versiones exactas de herramientas que en desarrollo local. No más "pero en mi máquina funciona", literalmente no puede diferir.

## Evaluación Honesta

Nix Flakes tiene desventajas reales:

- **La curva de aprendizaje es empinada**. El lenguaje Nix es raro. La documentación está dispersa. Los mensajes de error muchas veces no ayudan.
- **El ecosistema es más chico**. No se va a encontrar una respuesta en Stack Overflow para cada problema.
- **El soporte de macOS es de segunda clase**. Funciona, pero se va a encontrar edge cases que los usuarios de Linux nunca ven.
- **El uso de disco del Nix store crece**. Ejecutá ` nix store gc ` periódicamente o se va a perder 50GB antes de darte cuenta.

La conclusión: el tiempo invertido en aprender Nix se paga dentro del primer mes. Cada nuevo hire desde entonces es productivo desde el día uno. El CI se vuelve determinístico. Los builds son reproducibles entre plataformas. Y se borran 250 líneas de shell scripts que nadie entendía y todos tenían miedo de tocar.

Eso vale un lenguaje de programación raro.

## Preguntas frecuentes

### ¿Necesito aprender el lenguaje Nix para usar Flakes?

Lamentablemente sí, al menos lo básico. Nix como lenguaje es funcional y lazy, lo cual confunde. Pero para devShells, mayormente se necesita entender attribute sets, let bindings, y mkShell. La curva de aprendizaje es empinada pero corta.

### ¿Nix Flakes puede reemplazar completamente Docker para desarrollo?

Para manejo de dependencias y entornos reproducibles, absolutamente. Para case es donde se necesita replicar una versión específica del kernel Linux o testear configuraciones de red, Docker todavía tiene su lugar. En la práctica, la mayoría de los equipos pueden dejar Docker para desarrollo local.

### ¿Cómo afecta Nix Flakes la velocidad del pipeline CI/CD?

Los primeros builds son más lentos porque Nix descarga y compila más precisamente. Pero los builds subsiguientes son dramáticamente más rápidos gracias al binary cache. Los tiempos de CI pueden bajar de 8 minutos a 2 después de que el cache se calienta.

## Tips

1. **Tip:** Siempre fijar tu input ` nixpkgs ` a un commit específico o branch de release como ` nixpkgs/nixos-25.11 `, nunca uses ` nixpkgs/nixpkgs-unstable ` para proyectos de equipo.
2. **Tip:** Utilizar ` nix develop --impure ` mientras explorás, pero siempre eliminar ` --impure ` antes de commitear. Los shells impuros esconden dependencias faltantes que resultarán problemáticos en CI.
3. **Tip:** ` nix build .#dockerImage ` crea imágenes OCI sin Dockerfile ni daemon de Docker, perfecto para entornos CI que solo tienen Nix.
4. **Tip:** Cuando un build de Nix falla de forma críptica, ` nix log ` sobre el path de derivación fallido te da el output real que ` nix build ` se traga.

- nix
- flakes
- devops
- docker
- reproducible-builds
- developer-experience
- cross-compilation

[Volver al Blog](<https://lucioduran.com/es/blog>)
