forked from axeman/erpnext-nix
teutat3s
ceb04d96de
BindReadOnlyPaths This should fix temporary name resolution errors observed in erpnext. Minor cleanup and explanatory comment for confinement.packages We don't use pkgs from path, but prefer explicitly referring to pkgs
462 lines
14 KiB
Nix
462 lines
14 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
with lib;
|
|
let
|
|
cfg = config.services.erpnext;
|
|
pkg = cfg.package;
|
|
defaultUser = "erpnext";
|
|
in
|
|
{
|
|
# interface
|
|
options.services.erpnext = {
|
|
enable = mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = lib.mdDoc ''
|
|
Enable ERPNext.
|
|
|
|
When started, the ERPNext database is automatically created if it doesn't
|
|
exist.
|
|
'';
|
|
};
|
|
|
|
domain = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = lib.mdDoc ''
|
|
Domain name of your server.
|
|
'';
|
|
};
|
|
|
|
workDir = mkOption {
|
|
type = types.str;
|
|
default = "/var/lib/erpnext";
|
|
description = lib.mdDoc "Working directory of ERPNext.";
|
|
};
|
|
|
|
benchDir = mkOption {
|
|
type = types.str;
|
|
default = "${cfg.workDir}/bench";
|
|
description = lib.mdDoc "Bench directory for ERPNext.";
|
|
};
|
|
|
|
adminPasswordFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
example = "/run/secrets/erpnext-admin-password";
|
|
description = lib.mdDoc ''
|
|
A file containing the Administrator user password.
|
|
'';
|
|
};
|
|
|
|
database = {
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = lib.mdDoc "Database host address.";
|
|
};
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 3306;
|
|
description = lib.mdDoc "Database host port.";
|
|
};
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = "erpnext";
|
|
description = lib.mdDoc "Database name.";
|
|
};
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "erpnext";
|
|
description = lib.mdDoc "Database username.";
|
|
};
|
|
userPasswordFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
example = "/run/secrets/erpnext-db-user-password";
|
|
description = lib.mdDoc ''
|
|
A file containing the MariaDB erpnext user password.
|
|
'';
|
|
};
|
|
rootPasswordFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
example = "/run/secrets/erpnext-db-root-password";
|
|
description = lib.mdDoc ''
|
|
A file containing the MariaDB root user password.
|
|
'';
|
|
};
|
|
createLocally = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = lib.mdDoc "Create the database and database user locally.";
|
|
};
|
|
automaticMigrations = mkEnableOption
|
|
(lib.mdDoc "automatic migrations for database schema and data") // {
|
|
default = true;
|
|
};
|
|
};
|
|
|
|
redis = {
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = lib.mdDoc "Redis host address.";
|
|
};
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 6379;
|
|
description = lib.mdDoc "Redis host port.";
|
|
};
|
|
createLocally = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = lib.mdDoc "Create the redis server locally.";
|
|
};
|
|
};
|
|
|
|
socketIoPort = mkOption {
|
|
type = types.port;
|
|
default = 9000;
|
|
description = lib.mdDoc "Local socket.io HTTP server port.";
|
|
};
|
|
|
|
webserver = {
|
|
bindAddress = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = lib.mdDoc "Web interface address.";
|
|
};
|
|
bindPort = mkOption {
|
|
type = types.port;
|
|
default = 9090;
|
|
description = lib.mdDoc "Web interface port.";
|
|
};
|
|
};
|
|
|
|
caddy = mkOption {
|
|
type = types.nullOr types.attrs;
|
|
default = null;
|
|
example = lib.literalExpression ''
|
|
{
|
|
serverAliases = [
|
|
"erpnext.your.domain"
|
|
"erp.your.domain"
|
|
];
|
|
# Disable access logs
|
|
logFormat = ''
|
|
output discard
|
|
'';
|
|
}
|
|
'';
|
|
description = lib.mdDoc ''
|
|
With this option, you can customize a caddy virtual host.
|
|
Set to {} if you do not need any customization to the virtual host.
|
|
If enabled, then by default, the {option}`hostName` is
|
|
`''${domain}`,
|
|
TLS is active by default, and handled by caddy.
|
|
|
|
Additionally, you probably want to set the caddy email option, when
|
|
enabling this: {option}`services.caddy.email
|
|
|
|
If this is set to null (the default), no caddy virtualHost will be
|
|
configured.
|
|
'';
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = defaultUser;
|
|
description = lib.mdDoc "User under which ERPNext runs.";
|
|
};
|
|
|
|
package = mkOption {
|
|
type = types.package;
|
|
default = pkgs.python3.pkgs.erpnext;
|
|
defaultText = literalExpression "pkgs.python3.pkgs.erpnext";
|
|
description = lib.mdDoc "The ERPNext package to use.";
|
|
};
|
|
};
|
|
|
|
# implementation
|
|
config =
|
|
let
|
|
penv = pkgs.python3.buildEnv.override {
|
|
extraLibs = [
|
|
pkgs.python3.pkgs.frappe
|
|
pkgs.python3.pkgs.erpnext
|
|
pkgs.python3.pkgs.bench
|
|
];
|
|
};
|
|
appsFile = pkgs.writeText "erpnext-apps.txt" ''
|
|
frappe
|
|
erpnext
|
|
'';
|
|
# In a module, this could be provided by a use as a file as it could
|
|
# contain secrets and we don't want this in the nix-store. But here it
|
|
# is OK.
|
|
commonSiteConfig = {
|
|
db_host = "${cfg.database.host}";
|
|
db_port = "${toString cfg.database.port}";
|
|
db_name = "${cfg.database.name}";
|
|
db_password = "#NIXOS_ERPNEXT_DB_USER_PASSWORD#";
|
|
redis_cache = "redis://${cfg.redis.host}:${toString cfg.redis.port}?db=1";
|
|
redis_queue = "redis://${cfg.redis.host}:${toString cfg.redis.port}?db=2";
|
|
redis_socketio = "redis://${cfg.redis.host}:${toString cfg.redis.port}?db=0";
|
|
socketio_port = "${toString cfg.socketIoPort}";
|
|
};
|
|
commonSiteConfigFile = pkgs.writeText "erpnext-common_site_config.json" (builtins.toJSON commonSiteConfig);
|
|
|
|
defaultServiceConfig = {
|
|
User = cfg.user;
|
|
NoNewPrivileges = true;
|
|
Type = "simple";
|
|
BindReadOnlyPaths = [
|
|
"/etc/hosts:/etc/hosts"
|
|
"/etc/resolv.conf:/etc/resolv.conf"
|
|
"/etc/ssl:/etc/ssl"
|
|
"/etc/static/ssl:/etc/static/ssl"
|
|
"/run/agenix:/run/agenix"
|
|
"${pkgs.frappe-app}:${pkgs.frappe-app}"
|
|
"${pkgs.frappe-app}/share/apps/frappe:${cfg.benchDir}/apps/frappe"
|
|
"${pkgs.erpnext-app}:${pkgs.erpnext-app}"
|
|
"${pkgs.erpnext-app}/share/apps/erpnext:${cfg.benchDir}/apps/erpnext"
|
|
"${pkgs.frappe-erpnext-assets}/share/sites/assets:${cfg.benchDir}/sites/assets"
|
|
"${appsFile}:${cfg.benchDir}/sites/apps.txt"
|
|
"${penv}:${cfg.benchDir}/env"
|
|
];
|
|
WorkingDirectory = "${cfg.benchDir}";
|
|
# Expands to /var/lib/erpnext, see: 'man 5 systemd.exec'
|
|
StateDirectory = "erpnext";
|
|
};
|
|
in mkIf cfg.enable
|
|
{
|
|
services.mysql = mkIf cfg.database.createLocally {
|
|
enable = true;
|
|
package = pkgs.mariadb;
|
|
ensureUsers = [{
|
|
name = "root";
|
|
ensurePermissions = {
|
|
"*.*" = "ALL PRIVILEGES";
|
|
};
|
|
}];
|
|
ensureDatabases = [ "root" ];
|
|
};
|
|
|
|
services.redis.servers = mkIf cfg.redis.createLocally {
|
|
# Queue, naming it "" makes it use default values.
|
|
"".enable = true;
|
|
};
|
|
|
|
users = optionalAttrs (cfg.user == defaultUser) {
|
|
users.${defaultUser} = {
|
|
description = "User to run ERPNext";
|
|
group = defaultUser;
|
|
uid = 327;
|
|
# TODO assign an appropriate ID when merging this into nixos/nixpkgs
|
|
#uid = config.ids.uids.erpnext;
|
|
home = cfg.workDir;
|
|
};
|
|
|
|
groups.${defaultUser} = {
|
|
gid = 327;
|
|
# TODO assign an appropriate ID when merging this into nixos/nixpkgs
|
|
#gid = config.ids.gids.erpnext;
|
|
};
|
|
};
|
|
|
|
systemd.services.erpnext-setup-mysql = mkIf cfg.database.createLocally {
|
|
enable = true;
|
|
before = [ "erpnext-web.service" ];
|
|
after = [ "mysql.service" ];
|
|
wantedBy = [ "erpnext-web.service" ];
|
|
partOf = [ "erpnext-web.service" ];
|
|
script = ''
|
|
${pkgs.mariadb-client}/bin/mysql -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('$(cat "${cfg.database.rootPasswordFile}")')";
|
|
'';
|
|
serviceConfig = {
|
|
RemainAfterExit = true;
|
|
Type = "oneshot";
|
|
};
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d '${cfg.benchDir}/apps' 0750 ${cfg.user} ${config.users.users.${cfg.user}.group}"
|
|
"d '${cfg.benchDir}/config/pids' 0750 ${cfg.user} ${config.users.users.${cfg.user}.group}"
|
|
"d '${cfg.benchDir}/logs' 0750 ${cfg.user} ${config.users.users.${cfg.user}.group}"
|
|
"d '${cfg.benchDir}/sites' 0750 ${cfg.user} ${config.users.users.${cfg.user}.group}"
|
|
];
|
|
|
|
systemd.services.erpnext-nodejs-socketio = {
|
|
enable = true;
|
|
after = [ "erpnext-web.service" ];
|
|
wantedBy = [ "erpnext-web.service" ];
|
|
partOf = [ "erpnext-web.service" ];
|
|
description = "ERPNext Node.js HTTP server for socket.io ";
|
|
confinement = {
|
|
enable = true;
|
|
packages = [ pkgs.nodejs ];
|
|
};
|
|
serviceConfig = defaultServiceConfig // {
|
|
ExecStart = ''
|
|
${pkgs.nodejs}/bin/node ${cfg.benchDir}/apps/frappe/socketio.js
|
|
'';
|
|
};
|
|
};
|
|
|
|
services.caddy.enable = mkIf (cfg.caddy != null) true;
|
|
services.caddy.virtualHosts."${cfg.domain}" = mkIf (cfg.caddy != null) (lib.mkMerge [
|
|
cfg.caddy
|
|
({
|
|
extraConfig = ''
|
|
handle /assets/* {
|
|
root * ${pkgs.frappe-erpnext-assets}/share/sites
|
|
file_server
|
|
}
|
|
handle /socket.io/* {
|
|
reverse_proxy :${toString cfg.socketIoPort}
|
|
}
|
|
|
|
reverse_proxy :${toString cfg.webserver.bindPort}
|
|
'';
|
|
})
|
|
]);
|
|
|
|
systemd.services.erpnext-web = {
|
|
enable = true;
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [
|
|
"mysql.service"
|
|
"redis.service"
|
|
"redis-socketio.service"
|
|
"systemd-tmpfiles-setup.service"
|
|
];
|
|
description = "ERPNext web server";
|
|
confinement = {
|
|
enable = true;
|
|
# pkgs listed here get added to the services' BindReadOnlyPaths
|
|
# The same is true for pkgs referred to in ExecStartPre, ExecStart, etc.
|
|
# Explicitily listing these pkgs here for visibility
|
|
packages = [
|
|
penv
|
|
pkgs.coreutils
|
|
# Dependency for 'bench new-site' subcommand
|
|
pkgs.mariadb-client
|
|
pkgs.replace-secret
|
|
];
|
|
};
|
|
environment = {
|
|
PYTHON_PATH = "${penv}/${pkgs.python3.sitePackages}";
|
|
};
|
|
serviceConfig = defaultServiceConfig // {
|
|
TimeoutStartSec = "300s";
|
|
Restart = "on-failure";
|
|
ExecStartPre = assert cfg.adminPasswordFile != null && cfg.database.rootPasswordFile != null; pkgs.writeScript "erpnext-web-init" ''
|
|
#!/bin/sh
|
|
if ! test -e ${escapeShellArg "${cfg.workDir}/.db-created"}; then
|
|
# Fail on error
|
|
set -e
|
|
|
|
${pkgs.coreutils}/bin/install -m0600 ${commonSiteConfigFile} ${cfg.benchDir}/sites/common_site_config.json
|
|
${pkgs.replace-secret}/bin/replace-secret \
|
|
'#NIXOS_ERPNEXT_DB_USER_PASSWORD#' \
|
|
${cfg.database.userPasswordFile} \
|
|
${cfg.benchDir}/sites/common_site_config.json
|
|
|
|
ADMIN_PASSWORD="$(${pkgs.coreutils}/bin/cat "${cfg.adminPasswordFile}")"
|
|
DB_ROOT_PASSWORD="$(${pkgs.coreutils}/bin/cat "${cfg.database.rootPasswordFile}")"
|
|
|
|
# Upstream initializes the database with this command
|
|
${penv}/bin/bench new-site ${cfg.domain} \
|
|
--mariadb-root-password "$DB_ROOT_PASSWORD" \
|
|
--admin-password "$ADMIN_PASSWORD" \
|
|
--install-app erpnext
|
|
|
|
${pkgs.coreutils}/bin/touch ${escapeShellArg "${cfg.workDir}/.db-created"}
|
|
fi
|
|
|
|
${lib.optionalString cfg.database.automaticMigrations ''
|
|
# Migrate the database
|
|
${penv}/bin/bench --site ${cfg.domain} migrate
|
|
''}
|
|
'';
|
|
ExecStart = ''
|
|
${penv}/bin/gunicorn \
|
|
--chdir="${cfg.benchDir}/sites" \
|
|
--bind=${cfg.webserver.bindAddress}:${toString cfg.webserver.bindPort} \
|
|
--threads=4 \
|
|
--workers=3 \
|
|
--worker-class=gthread \
|
|
--worker-tmp-dir=/dev/shm \
|
|
--timeout=120 \
|
|
--preload \
|
|
frappe.app:application
|
|
'';
|
|
};
|
|
};
|
|
systemd.services.erpnext-queue-short = {
|
|
enable = true;
|
|
after = [ "erpnext-web.service" ];
|
|
wantedBy = [ "erpnext-web.service" ];
|
|
partOf = [ "erpnext-web.service" ];
|
|
description = "ERPNext short queue server";
|
|
confinement = {
|
|
enable = true;
|
|
packages = [ penv ];
|
|
};
|
|
serviceConfig = defaultServiceConfig // {
|
|
ExecStart = ''
|
|
${penv}/bin/bench worker --queue short
|
|
'';
|
|
};
|
|
};
|
|
systemd.services.erpnext-queue-default = {
|
|
enable = true;
|
|
after = [ "erpnext-web.service" ];
|
|
wantedBy = [ "erpnext-web.service" ];
|
|
partOf = [ "erpnext-web.service" ];
|
|
description = "ERPNext default queue server";
|
|
confinement = {
|
|
enable = true;
|
|
packages = [ penv ];
|
|
};
|
|
serviceConfig = defaultServiceConfig // {
|
|
ExecStart = ''
|
|
${penv}/bin/bench worker --queue default
|
|
'';
|
|
};
|
|
};
|
|
systemd.services.erpnext-queue-long = {
|
|
enable = true;
|
|
after = [ "erpnext-web.service" ];
|
|
wantedBy = [ "erpnext-web.service" ];
|
|
partOf = [ "erpnext-web.service" ];
|
|
description = "ERPNext long queue server";
|
|
confinement = {
|
|
enable = true;
|
|
packages = [ penv ];
|
|
};
|
|
serviceConfig = defaultServiceConfig // {
|
|
ExecStart = ''
|
|
${penv}/bin/bench worker --queue long
|
|
'';
|
|
};
|
|
};
|
|
systemd.services.erpnext-scheduler = {
|
|
enable = true;
|
|
after = [ "erpnext-web.service" ];
|
|
wantedBy = [ "erpnext-web.service" ];
|
|
partOf = [ "erpnext-web.service" ];
|
|
description = "ERPNext scheduler server";
|
|
confinement = {
|
|
enable = true;
|
|
packages = [ penv ];
|
|
};
|
|
serviceConfig = defaultServiceConfig // {
|
|
ExecStart = ''
|
|
${penv}/bin/bench schedule
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
}
|