1 { config, lib, pkgs, ... }:
7 cfg = config.services.mysql;
9 isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
10 isOracle = lib.getName cfg.package == lib.getName pkgs.mysql80;
11 # Oracle MySQL has supported "notify" service type since 8.0
12 hasNotify = isMariaDB || (isOracle && versionAtLeast cfg.package.version "8.0");
15 "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
17 format = pkgs.formats.ini { listsAsDuplicateKeys = true; };
18 configFile = format.generate "my.cnf" cfg.settings;
24 (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd.")
25 (mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.")
26 (mkRemovedOptionModule [ "services" "mysql" "extraOptions" ] "Use services.mysql.settings.mysqld instead.")
27 (mkRemovedOptionModule [ "services" "mysql" "bind" ] "Use services.mysql.settings.mysqld.bind-address instead.")
28 (mkRemovedOptionModule [ "services" "mysql" "port" ] "Use services.mysql.settings.mysqld.port instead.")
37 enable = mkEnableOption "MySQL server";
41 example = literalExpression "pkgs.mariadb";
43 Which MySQL derivation to use. MariaDB packages are supported too.
51 User account under which MySQL runs.
54 If left as the default value this user will automatically be created
55 on system activation, otherwise you are responsible for
56 ensuring the user exists before the MySQL service starts.
65 Group account under which MySQL runs.
68 If left as the default value this group will automatically be created
69 on system activation, otherwise you are responsible for
70 ensuring the user exists before the MySQL service starts.
77 example = "/var/lib/mysql";
79 The data directory for MySQL.
82 If left as the default value of `/var/lib/mysql` this directory will automatically be created before the MySQL
83 server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions.
88 configFile = mkOption {
92 A configuration file automatically generated by NixOS.
95 Override the configuration file used by MySQL. By default,
96 NixOS generates one automatically from {option}`services.mysql.settings`.
98 example = literalExpression ''
99 pkgs.writeText "my.cnf" '''
101 datadir = /var/lib/mysql
102 bind-address = 127.0.0.1
105 !includedir /etc/mysql/conf.d/
110 settings = mkOption {
114 MySQL configuration. Refer to
115 <https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html>,
116 <https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html>,
117 and <https://mariadb.com/kb/en/server-system-variables/>
118 for details on supported values.
121 MySQL configuration options such as `--quick` should be treated as
122 boolean options and provided values such as `true`, `false`,
123 `1`, or `0`. See the provided example below.
126 example = literalExpression ''
129 key_buffer_size = "6G";
131 log-error = "/var/log/mysql_err.log";
132 plugin-load-add = [ "server_audit" "ed25519=auth_ed25519" ];
136 max_allowed_packet = "16M";
142 initialDatabases = mkOption {
143 type = types.listOf (types.submodule {
148 The name of the database to create.
152 type = types.nullOr types.path;
155 The initial schema of the database; if null (the default),
156 an empty database is created.
163 List of database names and their initial schemas that should be used to create databases on the first startup
164 of MySQL. The schema attribute is optional: If not specified, an empty database is created.
166 example = literalExpression ''
168 { name = "foodatabase"; schema = ./foodatabase.sql; }
169 { name = "bardatabase"; }
174 initialScript = mkOption {
175 type = types.nullOr types.path;
177 description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database.";
180 ensureDatabases = mkOption {
181 type = types.listOf types.str;
184 Ensures that the specified databases exist.
185 This option will never delete existing databases, especially not when the value of this
186 option is changed. This means that databases created once through this option or
187 otherwise have to be removed manually.
195 ensureUsers = mkOption {
196 type = types.listOf (types.submodule {
201 Name of the user to ensure.
204 ensurePermissions = mkOption {
205 type = types.attrsOf types.str;
208 Permissions to ensure for the user, specified as attribute set.
209 The attribute names specify the database and tables to grant the permissions for,
210 separated by a dot. You may use wildcards here.
211 The attribute values specfiy the permissions to grant.
212 You may specify one or multiple comma-separated SQL privileges here.
214 For more information on how to specify the target
215 and on which privileges exist, see the
216 [GRANT syntax](https://mariadb.com/kb/en/library/grant/).
217 The attributes are used as `GRANT ''${attrName} ON ''${attrValue}`.
219 example = literalExpression ''
221 "database.*" = "ALL PRIVILEGES";
222 "*.*" = "SELECT, LOCK TABLES";
230 Ensures that the specified users exist and have at least the ensured permissions.
231 The MySQL users will be identified using Unix socket authentication. This authenticates the Unix user with the
232 same name only, and that without the need for a password.
233 This option will never delete existing users or remove permissions, especially not when the value of this
234 option is changed. This means that users created and permissions assigned once through this option or
235 otherwise have to be removed manually.
237 example = literalExpression ''
241 ensurePermissions = {
242 "nextcloud.*" = "ALL PRIVILEGES";
247 ensurePermissions = {
248 "*.*" = "SELECT, LOCK TABLES";
257 type = types.enum [ "master" "slave" "none" ];
259 description = "Role of the MySQL server instance.";
262 serverId = mkOption {
265 description = "Id of the MySQL server instance. This number must be unique for each instance.";
268 masterHost = mkOption {
270 description = "Hostname of the MySQL master server.";
273 slaveHost = mkOption {
275 description = "Hostname of the MySQL slave server.";
278 masterUser = mkOption {
280 description = "Username of the MySQL replication user.";
283 masterPassword = mkOption {
285 description = "Password of the MySQL replication user.";
288 masterPort = mkOption {
291 description = "Port number on which the MySQL master server runs.";
299 ###### implementation
301 config = mkIf cfg.enable {
303 services.mysql.dataDir =
304 mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql"
307 services.mysql.settings.mysqld = mkMerge [
309 datadir = cfg.dataDir;
310 port = mkDefault 3306;
312 (mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
313 log-bin = "mysql-bin-${toString cfg.replication.serverId}";
314 log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
315 relay-log = "mysql-relay-bin";
316 server-id = cfg.replication.serverId;
317 binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ];
320 plugin-load-add = "auth_socket.so";
324 users.users = optionalAttrs (cfg.user == "mysql") {
326 description = "MySQL server user";
328 uid = config.ids.uids.mysql;
332 users.groups = optionalAttrs (cfg.group == "mysql") {
333 mysql.gid = config.ids.gids.mysql;
336 environment.systemPackages = [ cfg.package ];
338 environment.etc."my.cnf".source = cfg.configFile;
340 systemd.services.mysql = {
341 description = "MySQL Server";
343 after = [ "network.target" ];
344 wantedBy = [ "multi-user.target" ];
345 restartTriggers = [ cfg.configFile ];
347 unitConfig.RequiresMountsFor = cfg.dataDir;
350 # Needed for the mysql_install_db command in the preStart script
351 # which calls the hostname command.
355 preStart = if isMariaDB then ''
356 if ! test -e ${cfg.dataDir}/mysql; then
357 ${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
358 touch ${cfg.dataDir}/mysql_init
361 if ! test -e ${cfg.dataDir}/mysql; then
362 ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
363 touch ${cfg.dataDir}/mysql_init
368 # https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
369 if test -n "''${_WSREP_START_POSITION}"; then
370 if test -e "${cfg.package}/bin/galera_recovery"; then
371 VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
375 # The last two environment variables are used for starting Galera clusters
376 exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
380 # The super user account to use on *first* run of MySQL server
381 superUser = if isMariaDB then cfg.user else "root";
383 ${optionalString (!hasNotify) ''
384 # Wait until the MySQL server is available for use
385 while [ ! -e /run/mysqld/mysqld.sock ]
387 echo "MySQL daemon not yet started. Waiting for 1 second..."
392 if [ -f ${cfg.dataDir}/mysql_init ]
394 # While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not
395 # Since we don't want to run this service as 'root' we need to ensure the account exists on first run
396 ( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
397 echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;"
398 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
400 ${concatMapStrings (database: ''
401 # Create initial databases
402 if ! test -e "${cfg.dataDir}/${database.name}"; then
403 echo "Creating initial database: ${database.name}"
404 ( echo 'create database `${database.name}`;'
406 ${optionalString (database.schema != null) ''
407 echo 'use `${database.name}`;'
409 # TODO: this silently falls through if database.schema does not exist,
410 # we should catch this somehow and exit, but can't do it here because we're in a subshell.
411 if [ -f "${database.schema}" ]
413 cat ${database.schema}
414 elif [ -d "${database.schema}" ]
416 cat ${database.schema}/mysql-databases/*.sql
419 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
421 '') cfg.initialDatabases}
423 ${optionalString (cfg.replication.role == "master")
425 # Set up the replication master
428 echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
429 echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
430 echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
431 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
434 ${optionalString (cfg.replication.role == "slave")
436 # Set up the replication slave
439 echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
441 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
444 ${optionalString (cfg.initialScript != null)
446 # Execute initial script
447 # using toString to avoid copying the file to nix store if given as path instead of string,
448 # as it might contain credentials
449 cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
452 rm ${cfg.dataDir}/mysql_init
455 ${optionalString (cfg.ensureDatabases != []) ''
457 ${concatMapStrings (database: ''
458 echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
459 '') cfg.ensureDatabases}
460 ) | ${cfg.package}/bin/mysql -N
463 ${concatMapStrings (user:
465 ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
466 ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
467 echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
468 '') user.ensurePermissions)}
469 ) | ${cfg.package}/bin/mysql -N
473 serviceConfig = mkMerge [
475 Type = if hasNotify then "notify" else "simple";
476 Restart = "on-abort";
482 # Runtime directory and mode
483 RuntimeDirectory = "mysqld";
484 RuntimeDirectoryMode = "0755";
485 # Access write directories
486 ReadWritePaths = [ cfg.dataDir ];
488 CapabilityBoundingSet = "";
490 NoNewPrivileges = true;
492 ProtectSystem = "strict";
495 PrivateDevices = true;
496 ProtectHostname = true;
497 ProtectKernelTunables = true;
498 ProtectKernelModules = true;
499 ProtectControlGroups = true;
500 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
501 LockPersonality = true;
502 MemoryDenyWriteExecute = true;
503 RestrictRealtime = true;
504 RestrictSUIDSGID = true;
505 PrivateMounts = true;
506 # System Call Filtering
507 SystemCallArchitectures = "native";
509 (mkIf (cfg.dataDir == "/var/lib/mysql") {
510 StateDirectory = "mysql";
511 StateDirectoryMode = "0700";