Déployer Isso sur NixOS dans un conteneur Docker

Vincent Bernat

Ce court article documente la façon dont je fais tourner Isso, le système de commentaires utilisé par ce blog, à l’intérieur d’un conteneur Docker sur NixOS, une distribution Linux construite sur Nix. Nix est un gestionnaire de paquets déclaratif pour Linux et d’autres systèmes Unix.


Bien que NixOS 20.09 intègre une dérivation pour Isso, elle est malheureusement cassée et repose sur Python 2. Comme j’utilise également un fork d’Isso, j’ai construit ma propre dérivation, fortement inspirée de celle dans master1 :

issoPackage = with pkgs.python3Packages; buildPythonPackage rec {
  pname = "isso";
  version = "custom";

  src = pkgs.fetchFromGitHub {
    # Use my fork
    owner = "vincentbernat";
    repo = pname;
    rev = "vbe/master";
    sha256 = "0vkkvjcvcjcdzdj73qig32hqgjly8n3ln2djzmhshc04i6g9z07j";
  };

  propagatedBuildInputs = [
    itsdangerous
    jinja2
    misaka
    html5lib
    werkzeug
    bleach
    flask-caching
  ];

  buildInputs = [
    cffi
  ];

  checkInputs = [ nose ];

  checkPhase = ''
    ${python.interpreter} setup.py nosetests
  '';
};

Je veux faire tourner Isso via Gunicorn. À cet effet, je construis un environnement Python combinant Isso et Gunicorn. Je peux alors invoquer ce dernier avec "${issoEnv}/bin/gunicorn" à la manière d’un environnement Python virtuel

issoEnv = pkgs.python3.buildEnv.override {
    extraLibs = [
      issoPackage
      pkgs.python3Packages.gunicorn
      pkgs.python3Packages.gevent
    ];
};

Avant de construire une image Docker, je aussi dois définir le fichier de configuration d’Isso :

issoConfig = pkgs.writeText "isso.conf" ''
  [general]
  dbpath = /db/comments.db
  host =
    https://vincent.bernat.ch
    http://localhost:8080
  notify = smtp
  […]
'';

NixOS inclut un outil pratique permettant de construire une image Docker sans utiliser de Dockerfile :

issoDockerImage = pkgs.dockerTools.buildImage {
  name = "isso";
  tag = "latest";
  extraCommands = ''
    mkdir -p db
  '';
  config = {
    Cmd = [ "${issoEnv}/bin/gunicorn"
            "--name" "isso"
            "--bind" "0.0.0.0:${port}"
            "--worker-class" "gevent"
            "--workers" "2"
            "--worker-tmp-dir" "/dev/shm"
            "--preload"
            "isso.run"
          ];
    Env = [
      "ISSO_SETTINGS=${issoConfig}"
      "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
    ];
  };
};

Comme nous faisons référence à la dérivation issoEnv dans config.Cmd, toute la dérivation, y compris Isso et Gunicorn, est copiée à l’intérieur de l’image Docker. Il en va de même pour issoConfig, le fichier de configuration que nous avons créé précédemment, et pkgs.cacert, la dérivation contenant les certificats racines. L’image résultante pèse 171 Mo une fois installée, ce qui est comparable à l’image Debian Buster générée par le Dockerfile officiel.

NixOS dispose d’une abstraction pour déployer les conteneurs Docker. Elle n’est pas actuellement documentée dans le manuel de NixOS, mais vous pouvez consulter le code source du module pour connaître les options disponibles. J’ai choisi d’utiliser Podman au lieu de Docker comme moteur car cela évite d’avoir à lancer un démon supplémentaire.

virtualisation.oci-containers = {
  backend = "podman";
  containers = {
    isso = {
      image = "isso";
      imageFile = issoDockerImage;
      ports = ["127.0.0.1:${port}:${port}"];
      volumes = [
        "/var/db/isso:/db"
      ];
    };
  };
};

Un fichier de configuration pour systemd est automatiquement créé pour exécuter et superviser le conteneur :

$ systemctl status podman-isso.service
● podman-isso.service
     Loaded: loaded (/nix/store/a66gzqqwcdzbh99sz8zz5l5xl8r8ag7w-unit->
     Active: active (running) since Sun 2020-11-01 16:04:16 UTC; 4min 44s ago
    Process: 14564 ExecStartPre=/nix/store/95zfn4vg4867gzxz1gw7nxayqcl>
   Main PID: 14697 (podman)
         IP: 0B in, 0B out
      Tasks: 10 (limit: 2313)
     Memory: 221.3M
        CPU: 10.058s
     CGroup: /system.slice/podman-isso.service
             ├─14697 /nix/store/pn52xgn1wb2vr2kirq3xj8ij0rys35mf-podma>
             └─14802 /nix/store/7vsba54k6ag4cfsfp95rvjzqf6rab865-conmo>

nov. 01 16:04:17 web03 podman[14697]: container init (image=localhost/isso:latest)
nov. 01 16:04:17 web03 podman[14697]: container start (image=localhost/isso:latest)
nov. 01 16:04:17 web03 podman[14697]: container attach (image=localhost/isso:latest)
nov. 01 16:04:19 web03 conmon[14802]: INFO: connected to SMTP server
nov. 01 16:04:19 web03 conmon[14802]: INFO: connected to https://vincent.bernat.ch
nov. 01 16:04:19 web03 conmon[14802]: [INFO] Starting gunicorn 20.0.4
nov. 01 16:04:19 web03 conmon[14802]: [INFO] Listening at: http://0.0.0.0:8080 (1)
nov. 01 16:04:19 web03 conmon[14802]: [INFO] Using worker: gevent
nov. 01 16:04:19 web03 conmon[14802]: [INFO] Booting worker with pid: 8
nov. 01 16:04:19 web03 conmon[14802]: [INFO] Booting worker with pid: 9

Pour terminer, nous configurons Nginx pour qu’il transmette les requêtes à destination de comments.luffy.cx au conteneur. NixOS offre une intégration simple pour mettre en place un certificat géré par Let’s Encrypt.

services.nginx.virtualHosts."comments.luffy.cx" = {
  root = "/data/webserver/comments.luffy.cx";
  enableACME = true;
  forceSSL = true;
  extraConfig = ''
    access_log /var/log/nginx/comments.luffy.cx.log anonymous;
  '';
  locations."/" = {
    proxyPass = "http://127.0.0.1:${port}";
    extraConfig = ''
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_hide_header Set-Cookie;
      proxy_hide_header X-Set-Cookie;
      proxy_ignore_headers Set-Cookie;
    '';
  };
};
security.acme.certs."comments.luffy.cx" = {
  email = lib.concatStringsSep "@" [ "letsencrypt" "vincent.bernat.ch" ];
};

Bien que je rencontre encore des difficultés avec Nix et NixOS, je suis convaincu que c’est ainsi qu’une infrastructure déclarative doit être conçue. J’aime le fait d’avoir en un seul fichier contenant la dérivation pour construire Isso, la configuration, l’image Docker, la définition du conteneur et la configuration Nginx. Le langage Nix permet à la fois de construire des paquets et de gérer des configurations.

De plus, l’image Docker est mise à jour automatiquement comme un hôte NixOS normal. Cela résout un problème qui affecte l’écosystème Docker : plus d’images périmées ! Ma prochaine étape est de combiner cette approche avec Nomad, un orchestrateur pour déployer et gérer les conteneurs.

Mise à jour (01.2023)

Je n’utilise désormais plus une image Docker mais un conteneur systemd. Cela permet de partager /nix/store et de faire tourner Isso avec un utilisateur dynamique.


  1. Il y a une différence subtile : j’utilise buildPythonPackage au lieu de buildPythonApplication. C’est important pour l’étape suivante. Je n’ai pas trouvé de moyen de convertir une application en paquet. ↩︎