Nix e NixOS: reprodutibilidade total no ambiente de desenvolvimento

1. Introdução ao Ecossistema Nix

O problema da inconsistência entre ambientes é um dos maiores desafios no desenvolvimento de software moderno. A frase "funciona na minha máquina" tornou-se um meme que reflete uma dor real: diferenças sutis entre sistemas operacionais, versões de bibliotecas e configurações causam bugs que consomem horas de debugging.

Nix resolve esse problema de forma radical. Diferente de gerenciadores de pacotes tradicionais como APT ou Homebrew, o Nix Package Manager opera com pureza funcional: cada pacote é construído em um ambiente isolado, sem acesso a dependências não declaradas explicitamente. O resultado é armazenado no Nix Store (/nix/store) em diretórios nomeados por hash do conteúdo, garantindo que versões diferentes do mesmo pacote coexistam sem conflitos.

Já o NixOS é uma distribuição Linux completa que estende esse conceito para todo o sistema operacional. Enquanto o Nix gerencia pacotes em qualquer distribuição, o NixOS permite declarar todo o sistema — kernel, serviços, usuários, configurações de rede — como código em configuration.nix.

2. Instalação e Configuração Inicial do Nix

A instalação do Nix Package Manager é simples em qualquer distribuição Linux ou macOS:

sh <(curl -L https://nixos.org/nix/install)

Após a instalação, configure o canal de pacotes:

nix-channel --add https://nixos.org/channels/nixpkgs-unstable
nix-channel --update

Comandos essenciais para começar:

# Instalar um pacote isoladamente
nix-env -iA nixpkgs.htop

# Entrar em um shell com Python 3.11
nix-shell -p python311

# Construir um pacote a partir de uma expressão
nix-build default.nix

3. Criando Ambientes de Desenvolvimento Reproduzíveis com shell.nix

O arquivo shell.nix é a forma mais direta de criar ambientes isolados. Exemplo para um projeto web completo com Python, Node.js e PostgreSQL:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [
    python311
    python311Packages.pip
    python311Packages.virtualenv
    nodejs_20
    postgresql_16
    redis
    git
  ];

  shellHook = ''
    echo "Ambiente de desenvolvimento carregado!"
    export PG_DATA="$PWD/.pgdata"
    if [ ! -d "$PG_DATA" ]; then
      initdb "$PG_DATA" -E UTF8 --locale=en_US.UTF-8
    fi
    pg_ctl -D "$PG_DATA" -l "$PG_DATA/logfile" start
  '';
}

Execute com nix-shell e todas as ferramentas estarão disponíveis, com o PostgreSQL iniciado automaticamente.

4. Gerenciamento de Versões e Dependências com Flakes

Nix Flakes elevam a reprodutibilidade a outro nível. O arquivo flake.nix é auto-contido e inclui um flake.lock que fixa cada dependência transitiva:

{
  description = "Ambiente de desenvolvimento web";

  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 {
          packages = with pkgs; [
            go_1_22
            gopls
            delve
            postgresql_16
          ];
        };
      }
    );
}

Vantagens sobre shell.nix:
- Reprodutibilidade total: o lockfile garante que todos usem exatas mesmas versões
- Integração com Git: o flake.lock é versionado junto com o código
- Composição: flakes podem referenciar outros flakes como dependências

5. Integração com Ferramentas de Desenvolvimento

Direnv + Nix ativa automaticamente o ambiente ao entrar no diretório:

# .envrc
use flake
# Instalar direnv e hook no shell
nix-env -iA nixpkgs.direnv
eval "$(direnv hook bash)"

Para editores, é possível configurar o Nix como backend de pacotes. No VSCode, use extensões como "Nix Environment Selector" para detectar automaticamente ambientes declarados.

Compartilhe ambientes via cache binário:

# Construir e enviar para cache
nix build .#devShells.x86_64-linux.default
nix copy --to 's3://meu-cache?region=us-east-1' ./result

6. NixOS: Configuração Declarativa de Sistemas Completos

No NixOS, um configuration.nix declara todo o sistema. Exemplo de servidor de desenvolvimento:

{ config, pkgs, ... }:

{
  imports = [ ./hardware-configuration.nix ];

  environment.systemPackages = with pkgs; [
    git
    vim
    curl
    docker
  ];

  services.postgresql = {
    enable = true;
    package = pkgs.postgresql_16;
    enableTCPIP = true;
    authentication = pkgs.lib.mkForce ''
      local all all trust
      host all all 127.0.0.1/32 trust
    '';
  };

  services.redis = {
    enable = true;
    bind = "127.0.0.1";
  };

  services.nginx = {
    enable = true;
    virtualHosts."dev.local" = {
      root = "/var/www/dev";
      locations."/" = {
        proxyPass = "http://localhost:3000";
      };
    };
  };
}

Ambientes isolados podem ser criados com nixos-container:

nixos-container create dev-env --config ./container-config.nix
nixos-container start dev-env

7. Boas Práticas e Solução de Problemas Comuns

Pacotes não disponíveis no nixpkgs: use overlays ou fetchFromGitHub:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = [
    (pkgs.callPackage ./meu-pacote.nix {})
    (pkgs.fetchFromGitHub {
      owner = "user";
      repo = "repo";
      rev = "v1.0.0";
      sha256 = "sha256-...";
    })
  ];
}

Debug de builds: quando um build falha, use:

nix-build --keep-failed default.nix
# Examine /tmp/nix-build-*.drv-0/

Migração gradual: comece usando nix-shell para projetos críticos, depois evolua para flakes. Para projetos existentes, crie um shell.nix que espelhe as dependências atuais.

8. Comparação com Alternativas e Casos de Uso Avançados

Nix vs Docker: Docker isola em nível de sistema operacional, Nix isola em nível de pacotes. Use Docker para ambientes completos (sistemas legados, dependências binárias pesadas), Nix para ambientes de desenvolvimento onde precisão de versões e cache local são críticos.

Nix vs gerenciadores específicos (Poetry, Bundler, npm): esses gerenciadores fixam dependências de alto nível, mas não garantem que as mesmas bibliotecas de sistema estejam presentes. Nix gerencia tudo — desde o compilador C até a versão do Python.

Casos reais:
- CI/CD com Nix: builds são deterministicamente reproduzíveis, eliminando "funciona no CI, não funciona local"
- Deploy de ambientes de desenvolvimento completos: um único flake.nix pode definir toolchains para múltiplas linguagens
- Ambientes de pesquisa: fixar versões exatas de bibliotecas científicas para reprodutibilidade de experimentos

Referências