Nix : Environnement de développement Dolibarr

Bonjour tout le monde,

Nouveau pc, nouvel OS oblige. Je me lance donc à la découverte de NixOS avec en tête la reproductibilité qu’il peut offrir.

J’ouvre ce sujet pour lancer la discussion, mais je ferai aussi un dépôt public prochainement pour partager ma configuration Nix :slight_smile:

Bonne journée !

@hop t’es toujours dans le coin ? voici un nouvel adepte de nix !!!

J’ai un doute pour une utilisation journalière, tu risques de passer plus de temps à configurer ton env plutôt que de travailler :slight_smile: .

J’aime bien le principe de l’isolation avec nix-shell au niveau projet, mais je n’irais pas plus loin avec nix :wink:

de toute façon, c’est un nouveau PC, donc ça se formate vite si c’est vraiment galère

Je me laisse la soirée pour avoir un fonctionnement qui me convient et avoir assimilé suffisamment de notions pour m’en sortir. Sinon on retournera aux bases : Debian testing.

Linux Mint est bien, enfin surtout Cinnamon comme env de bureau, niveau productivité, il n’y a pas mieux, je trouve.

1 « J'aime »

i3wm :heart:

NixOs + i3wm + LazyVim

Tu es un extrémiste :rofl:

Rassure-moi, ton navigateur, ce n’est pas lynx ?

:rofl: ha … parce-qu’il faut autre chose pour coder dolibarr ? w3m à la limite haute non ?

1 « J'aime »

LazyVim, c’est uniquement par économie de ressources, mais ça reste très très proche de codium, en moins lourd.

i3wm, ce sont des restes mais une fois que tu l’as en main, c’est comme Vim et associés : tu gagnes un temps de fou !

Ça fait bien longtemps que pour le boulot, je ne regarde plus les ressources…
PhpStorm, datagrip, refact pour la complétion par IA. 64 Go de RAM, carte graphique de 16 Go et ça arrive de me sentir à l’étroit :grin:

C’est mon côté minimaliste, pour composer la place que je prends dans le monde non numérique :rofl:

@bfaliere il y a tellement de souffrance dans ce monde, pourquoi s’automutiler inutilement ? :wink:

Personne n’utilise Emacs comme éditeur ? Emacs 29.4

Le plaisir, voyons, quoi d’autre ? :wink:

Bonjour,

Je déterre le sujet, n’ayant pas trouvé grand chose d’autre qui parle de NixOS et Dolibarr sur ce forum.
Est-ce que le dépôt public avec ta configuration nixos existe quelque part ?

Bonjour,

Malheureusement j’ai dû interrompre mes activités plusieurs mois après ce post, et n’ai pas encore eu le temps de me repencher sur le sujet, mais il est toujours dans un coin de ma tête…

Ok, merci pour la réponse :smiley:

1 « J'aime »

Après, si jamais tu as envie de te lancer sur le sujet, je peux ouvrir un dépôt, mais pour l’heure je n’ai rien à mettre dedans.

Je n’ai rien de vraiment spécifique pour Dolibarr dans ma config nixos, c’est une minuscule partie de ma config pour le dev php.

1 « J'aime »

Si ça intéresse quelqu’un voici ma config nix qui me permet de déployer plusieurs instances de dolibarr sur un serveur

{
  lib,
  config,
  sources,
  pkgs,
  ...
}:

let
  inherit (config.networking) hostName;
  inherit (config) bucketHash;
  inherit (config) server-isProd;
  app = "dolibarr";
  cfg = config.app-dolibarr;
  phps = import sources.nix-phps;
  appDir = "/srv/www/${app}";
  pwd = config.age.secrets."${app}.mysql.pwd".path;
  dolPwd = config.age.secrets."${app}.dolibarr.pwd".path;

  # On construit la liste des BDD nécessaires depuis la configuration du module
  # et on s'assure que le user "app" a bien les droits sur ces BDD
  ensureDatabases = builtins.catAttrs "database" cfg.erpList;
  databasePrivileges = builtins.concatStringsSep "\n" (
    builtins.map (
      db: ''echo  "GRANT ALL PRIVILEGES ON ${db}.* TO '${app}'@'localhost';"''
    ) ensureDatabases
  );
  # Configuration des virtuals hosts
  vhost = erp: {
    name = erp.domain;
    value = {
      forceSSL = true;
      enableACME = true;
      acmeRoot = null; # DNS-01 challenge
      root = "${appDir}/${erp.company}/${erp.dolVersion}/htdocs";
      extraConfig = ''
        index index.php;
        error_page 404 /index.php?controller=404;
      '';

      locations = {
        "~ /\\." = {
          extraConfig = ''
            deny all;
          '';
        };

        "~ \\.(log|tpl|twig|sass|yml)$" = {
          extraConfig = ''
            deny all;
          '';
        };

        "~ ^(.+\\.php)(.*)$" = {
          # Check that the PHP script exists before passing it
          # tryFiles will reset path_info
          # tryFiles = "$fastcgi_script_name =404";
          # fix see https://trac.nginx.org/nginx/ticket/321
          # set $path_info $fastcgi_path_info;
          # fastcgi_param PATH_INFO $path_info;

          # see https://github.com/Dolibarr/dolibarr/issues/6163#issuecomment-391265538
          extraConfig = ''
            # adresses ip autorisées à accéder à l'application
            ${pkgs.lib.ph-nginxAllowBlock cfg.allowedIps}
            # socket
            fastcgi_pass unix:${config.services.phpfpm.pools."dolibarr-${erp.company}".socket};

            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            if (!-f $document_root$fastcgi_script_name) {
                return 404;
            }

            # Utilisé par tcpdf
            fastcgi_param PATH_TRANSLATED $document_root$fastcgi_script_name;

            ## nécessaire pour modules custom dolibarr
            fastcgi_param CONTEXT_DOCUMENT_ROOT $document_root;

            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            # Dolibarr Rest API path support
            fastcgi_param PATH_INFO $fastcgi_path_info;

            include ${config.services.nginx.package}/conf/fastcgi_params;

            ## TUNE buffers to avoid error ##
            fastcgi_buffers 16 32k;
            fastcgi_buffer_size 64k;
            fastcgi_busy_buffers_size 64k;
          '';
        };
      };
    };
  };
  virtualHosts = builtins.listToAttrs (builtins.map vhost cfg.erpList);
  # Configuration des pools phpfpm
  pool = erp: {
    name = "dolibarr-${erp.company}";
    value = {
      phpPackage = phps.packages.${builtins.currentSystem}.${erp.phpVersion}.buildEnv {
        extensions = { enabled, all }: enabled ++ (with all; [ ]);
        # recommandé par dolibarr
        extraConfig = ''
          memory_limit = 1G
          session.use_strict_mode = 1
          session.cookie_samesite = "Lax"
          allow_url_fopen = 0
          disable_functions = "pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals"
        '';
      };
      user = "${app}";
      settings = {
        "listen.owner" = config.services.nginx.user;
        "listen.group" = config.services.nginx.group;
        "listen.mode" = "0660";
        "pm" = "dynamic";
        "pm.max_children" = 32;
        "pm.max_requests" = 500;
        "pm.start_servers" = 2;
        "pm.min_spare_servers" = 2;
        "pm.max_spare_servers" = 5;
        "php_admin_value[error_log]" = "stderr";
        "php_admin_flag[log_errors]" = true;
        "catch_workers_output" = true;
      };
      phpOptions = ''
        post_max_size = 10M
        upload_max_filesize = 10M
      '';
    };
  };
  phpfpmPools = builtins.listToAttrs (builtins.map pool cfg.erpList);

  configFile =
    erp:
    (pkgs.substituteAll {
      src = ./conf.php;
      dolibarr_main_url_root = "https://${erp.domain}";
      dolibarr_main_document_root = "${appDir}/${erp.company}/${erp.dolVersion}/htdocs";
      dolibarr_main_document_root_alt = "${appDir}/${erp.company}/${erp.dolVersion}/htdocs/custom";
      dolibarr_main_data_root = "${appDir}/${erp.company}/documents";
      dolibarr_main_db_file_path = "${dolPwd}";
      dolibarr_main_db_name = "${erp.database}";
    });

  rule = erp: [
    "d ${appDir}/${erp.company} 0750 ${app} ${app} - "
    "d ${appDir}/${erp.company}/documents 0750 ${app} ${app} - "
    "f ${appDir}/${erp.company}/documents/install.lock 0440 ${app} ${app} - "
    "C ${appDir}/${erp.company}/${erp.dolVersion} 0750 ${app} ${app} - ${sources.${erp.dolVersion}}"
    "C ${appDir}/${erp.company}/${erp.dolVersion}/htdocs/conf/conf.php 0400 ${app} ${app} - ${(configFile erp)}"
    "L+ ${appDir}/${erp.company}/${erp.dolVersion}/htdocs/custom/multicompany - - - - ${appDir}/modules/multicompany"
  ];

  tmpfilesRules = builtins.concatLists (builtins.map rule cfg.erpList);

  backupPath = erp: "${appDir}/${erp.company}/documents";
  resticBackupPath = (builtins.map backupPath cfg.erpList);

  systemdDolibarrScripts = builtins.concatStringsSep "\n" (
    builtins.map (erp: ''
      ${pkgs.util-linux}/bin/runuser -u ${app} -- \
        ${config.services.phpfpm.pools."dolibarr-${erp.company}".phpPackage}/bin/php -f \
        ${appDir}/${erp.company}/${erp.dolVersion}/scripts/cron/cron_run_jobs.php ${erp.cronSecurityKey} admin > ${appDir}/${erp.company}/documents/cron_run_jobs.php.log 2>&1
    '') cfg.erpList
  );

  # Modules à installer
  multicompany = builtins.path { path = ./modules/module_multicompany-18.0.7.zip;};
  modules = [ multicompany ];
in
{
  ##
  imports = [ ];
  ##
  options = {
    app-dolibarr = {
      enable = lib.mkEnableOption "application dolibarr";
      erpList = lib.mkOption {
        type = lib.types.listOf lib.types.attrs;
        default = [ ];
        description = "List of erp configurations.";
        example = ''
          [
            {
              company = "dolibarr";
              database = "dolibarr_company";
              domain = "dolibarr-company.example.com";
              phpVersion = "php81";
              dolVersion = "1805";
              cronSecurityKey = "secret-key"
            }
          ]
        '';
      };
      sshKeys = lib.mkOption {
        type = lib.types.listOf lib.types.str;
        default = [ ];
        description = "SSH keys to add to the user.";
      };
      # adresses ip autorisées à accéder à l'application
      allowedIps = lib.mkOption {
        type = lib.types.listOf lib.types.str;
        default = [ ];
      };
    };
  };

  ##
  config = lib.mkIf cfg.enable {
    # Configuration mot de passe user app
    age.secrets = {
      "${app}.mysql.pwd" = {
        file = ../../../machines/${hostName}/secrets/${app}/${app}.mysql.pwd;
        owner = "mysql";
      };
      "${app}.dolibarr.pwd" = {
        file = ../../../machines/${hostName}/secrets/${app}/${app}.mysql.pwd;
        owner = "${app}";
      };
    };
    users = {
      users.${app} = {
        isNormalUser = true;
        homeMode = "0750";
        home = appDir;
        group = "${app}";
        extraGroups = [ "nginx" ];
        openssh.authorizedKeys.keyFiles = builtins.map (file: ../../../keys + "/${file}") cfg.sshKeys;
      };
      groups.${app}.members = [
        "${app}"
        "restic"
      ];
      users.nginx.extraGroups = [ "${app}" ];
    };

    # Configuration des modules
    system.activationScripts.unzipModules = {
      text = ''
        for module in ${builtins.toString modules}; do
          if [ -f $module ]; then
            ${pkgs.unzip}/bin/unzip -o $module -d ${appDir}/modules
            chown --recursive ${app}: ${appDir}/modules
            find ${appDir}/modules -type f -exec chmod 440 {} +
            find ${appDir}/modules -type d -exec chmod 750 {} +
          fi
        done
      '';
    };

    systemd = {
      tmpfiles.rules = [
        "d ${appDir}/modules 0750 ${app} ${app} - "
      ] ++ tmpfilesRules ++ [ ];
      # timers systemd
      services = {
        dolibarr-taches-recurrentes = {
          serviceConfig = {
            Type = "oneshot";
          };
          script = '''' + systemdDolibarrScripts;
        };
      };
      # Tous les timers sont configurés sur le fuseau horaire UTC
      timers = lib.mkIf server-isProd {
        dolibarr-taches-recurrentes = {
          wantedBy = [ "timers.target" ];
          timerConfig = {
            OnCalendar = "*:0/15"; # toutes les 15 minutes
            Persistent = true;
          };
          unitConfig = {
            Description = "dolibarr tâches récurrentes";
            Wants = "network.target";
            After = "network.target";
          };
        };
      };
    };

    # Création du user "app" pour accès à la base de données
    # Alter user pour forcer le changement de mot de passe si le USER existe déjà
    systemd.services.mysql.postStart = ''
      ( echo "CREATE USER IF NOT EXISTS '${app}'@'localhost' IDENTIFIED BY '$(cat ${pwd})';"
        echo "ALTER USER '${app}'@'localhost' IDENTIFIED BY '$(cat ${pwd})';"
        ${databasePrivileges}
        echo "FLUSH PRIVILEGES;"
      ) | ${config.services.mysql.package}/bin/mysql -N
    '';

    services = {
      restic = {
        backups = {
          "${app}-files" = {
            user = "restic";
            repository = "rclone:ovh:restic.${hostName}.${bucketHash}/${app}";
            initialize = true;
            passwordFile = config.age.secrets."restic.pwd".path;
            rcloneConfigFile = config.age.secrets."rclone.conf".path;
            paths = [ ] ++ resticBackupPath;
            pruneOpts = [ "--keep-last 7" ];
            timerConfig = {
              OnCalendar = "*-*-* 20:00:00";
              Persistent = true;
              RandomizedDelaySec = "15min";
            };
          };
        };
      };
      mysql = {
        inherit ensureDatabases;
      };
      phpfpm.pools = phpfpmPools;
      nginx = {
        enable = true;
        virtualHosts = virtualHosts;
      };
    };
  };
}

2 « J'aime »