I need some advice on fine-tuning NixOS declarative container for WordPress on localhost

TLDR:

My final goal is to have a working config for WordPress related services on localhost as a nix declarative container with ability to automatically configure set of plugins and themes.
The whole wp-content directory from the container should be synced with my local host filesystem.

It should run these services:

  • nginx
  • phpfpm
  • mysql
  • wordpress
  • traefik ( the subdomain name .docker.localdev might be misleading, I don’t use docker in my nix container. I just had this name pre-configured in my system.

THE DETAILS:
I was trying translate this docker-compose to nix declarative container but without usage of docker:

sersion: '3'
services:
  mariadb:

    image: bitnami/mariadb:latest
    volumes:
      - 'mariadb_data:/bitnami/mariadb'
    restart: always
    environment:
      - MARIADB_ROOT_PASSWORD=wordpress
      - MARIADB_DATABASE=wordpress
      - MARIADB_USER=wordpress
      - MARIADB_PASSWORD=wordpress
    networks:
      - web
    healthcheck:
      test: [ 'CMD', '/opt/bitnami/scripts/mariadb/healthcheck.sh' ]
      interval: 15s
      timeout: 5s
      retries: 6

  wordpress:
    image: wordpress:latest
    # command: -H unix:///var/run/docker.sock
    ports:
      - 8080:80
    expose:
      - 8080
    security_opt:
      - no-new-privileges:true
    networks:
      - web

    depends_on:
      - mariadb
    restart: always
    environment:
      - WORDPRESS_DB_HOST=mariadb
      - WORDPRESS_DB_USER=wordpress
      - WORDPRESS_DB_PASSWORD=wordpress
      - WORDPRESS_DB_NAME=wordpress

    labels:
      - traefik.enable=true
      - traefik.docker.network=web
      - traefik.http.routers.wp-http.entrypoints=web
      - traefik.http.routers.wp-http.rule=Host(`wp.docker.localdev`)
      - traefik.http.routers.wp-http.middlewares=wp-https
      - traefik.http.middlewares.wp-https.redirectscheme.scheme=https
      - traefik.http.routers.wp-https.entrypoints=websecure
      - traefik.http.routers.wp-https.rule=Host(`wp.docker.localdev`)
      - traefik.http.routers.wp-https.tls=true
      # - traefik.http.services.wp.loadbalancer.server.port=8080
    volumes:
      - ./wp-content:/var/www/html/wp-content

volumes:
  db_data:
  mariadb_data:
    driver: local

networks:
  web:
    external: true

I am using extra-container, which can run declarative NixOS containers like imperative containers, without system rebuilds, from any folder.
I was fiddling around with nix container config, it partially work for me but i need some advice to fine tune it.


{ pkgs, lib, config, ... }:
let
  app = "wpdemo";
  socket = "/run/phpfpm/${app}.sock";
  domain = "localhost";
  dataDir = "/var/www/public";

in
{
  containers.wp = {
    config = {
      networking.firewall.enable = false;
      security.acme.defaults.email = "admin@docker.localdev";
      networking.firewall.allowedTCPPorts = [ 80 82 ];
      services.traefik = {
        enable = true;
        staticConfigOptions = {
          providers.docker = {
            exposedByDefault = false;
          };
          entryPoints.web.address = ":80";
        };
        dynamicConfigOptions = {
          http.routers.wp = {
            rule = "Host(`wp.docker.localdev`)";
            entryPoints = [ "web" ];
            service = "wp-service";
          };
          http.services.wp-service.loadBalancer.server.port = 82;
        };
      };

      services.phpfpm.pools.${app} = {
        user = app;
        settings = {
          "listen.owner" = "nginx";
          "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;
        };
        phpEnv."PATH" = lib.makeBinPath [ pkgs.php ];
      };

      services.mysql = {
        enable = true;
        package = pkgs.mysql;
      };

      services.wordpress = {
        webserver = "nginx";
        sites."wp.docker.localdev" = {
          themes =
            let
              storefrontTheme = pkgs.stdenv.mkDerivation {
                name = "storefront-theme";
                src = pkgs.fetchurl {
                  url = "https://downloads.wordpress.org/theme/storefront.4.2.0.zip";
                  sha256 = "J9q50EhIWflZNhE6gMt4YakFKhIMDORfWwauXHfiQ0A=";
                };
                nativeBuildInputs = [ pkgs.unzip ];
                installPhase = ''
                  mkdir -p $out; cp -R * $out/
                '';
              };
            in
            [ storefrontTheme ];
          plugins =
            let
              woocommercePlugin = pkgs.stdenv.mkDerivation {
                name = "woocommerce-plugin";
                src = pkgs.fetchurl {
                  url = "https://downloads.wordpress.org/plugin/woocommerce.7.4.1.zip";
                  sha256 = "a/I10n+J/mWa55fMsfi54CR2F0FPkDpEjStQiRPSgWY=";
                };
                nativeBuildInputs = [ pkgs.unzip ];
                installPhase = ''
                  mkdir -p $out; cp -R * $out/
                '';
              };
            in
            [ woocommercePlugin ];
          extraConfig = ''
            define('WP_POST_REVISIONS', 3);
          '';
        };
      };
      services.nginx = {
        enable = true;
        virtualHosts.${domain} = {
          listen = [{ addr = "127.0.0.1"; port = 80; }];
          serverName = "wp.docker.localdev";
          locations."/" = {
            root = /var/www/public;
            extraConfig = ''
              access_log off;
              charset utf-8;
              etag off;
              index index.php;
              location ~ \.php$ {
                  fastcgi_split_path_info ^(.+\.php)(/.+)$;
                  fastcgi_pass  unix:${socket};
                  include ${pkgs.nginx}/conf/fastcgi_params;
                  include ${pkgs.nginx}/conf/fastcgi.conf;
              }
            '';
          };
        };
      };

      users.users.${app} = {
        isSystemUser = true;
        createHome = true;
        home = dataDir;
        group = app;
      };
      users.groups.${app} = { };
    };
  };
}

The problems with this config:

  1. The nginx service root

         locations."/" = {
           root = /var/www/public;
         }
    
  • Current setup: i created /var/www/public on local file system and extracted wordpress manualy there so it copies files from my local system to the container.
  • Expected setup: Wordpress sholud be downloaded an run inside continer on /var/www/public without any manual steps.
  1. services.wordpress downloads woocomerce plugin and storefront theme to the nix store inside container
    Problem : i don’t have idea how to link it to /var/www/public in the container.
  • I tried copy or symlink it inside wordpress service
installPhase = ''
      mkdir -p $out; cp -R * $out/
      mkdir -p ${dataDir}/wp-content/plugins/woocommerce
      cp -R $out/* ${dataDir}/wp-content/plugins/woocommerce
     ''
installPhase = ''
    mkdir -p $out; cp -R * $out/
    mkdir -p ${dataDir}/wp-content/plugins/woocommerce
    ln -s  $out/ ${dataDir}/wp-content/plugins/woocommerce
     ''

got error: mkdir: cannot create directory '/var': Permission denied
i have issues with permissions, how to fix it in a declarative way?

  • Current setup: manually added plugins
  • Expected setup: wordpress user and db should be crated automatically
  1. The services.mysql. i tried to add this config to create database automatically, but i couldn’t connect to DB.
       services.mysql = {
        enable = true;
        package = pkgs.mariadb;
        initialScript =
          pkgs.writeText "initial-script" ''
            CREATE USER 'admin'@localhost IDENTIFIED BY 'password';
            ALTER USER 'admin'@'localhost' IDENTIFIED BY 'password';
            grant all privileges on *.* to 'admin'@localhost identified by 'password';
            CREATE DATABASE wordpress;
          '';

        ensureDatabases = [
          "wordpress"
        ];
        ensureUsers = [
          {
            name = "admin";
            ensurePermissions = {
              "admin.*" = "ALL PRIVILEGES";
              "*.*" = "ALL PRIVILEGES";
            };
          }
        ];
      };
  • Current setup: Create WordPress user and db manually from MySQL CLI
  • Expected setup: WordPress user and db should be crated automatically
  1. sync the contents of wp-content inside container to my local system . i-do-nix suggested my the working solution for this:
containers.abc = {
  bindMounts = {
    "/path/in/container" = {
      hostPath = "/path/on/host";
      isReadOnly = true;
    };
  };
  # config = ...
};

However, first i must solve the previous issues. Thank in advance for any tips or suggestions.

1 Like

I managed to fix most of issues, except sync config:

Here is the working container wp.nix

{ pkgs, lib, config, ... }:
let
  app = "wpdemo";
  socket = "/run/phpfpm/${app}.sock";
  domain = "localhost";
in
{
  containers.wp = {


    config = {
      networking.firewall.enable = false;
      security.acme.defaults.email = "admin@docker.localdev";
      networking.firewall.allowedTCPPorts = [ 80 82 ];
      services.traefik = {
        enable = true;
        staticConfigOptions = {
          providers.docker = {
            exposedByDefault = false;
          };
          entryPoints.web.address = ":80";
        };
        dynamicConfigOptions = {
          http.routers.wp = {
            rule = "Host(`wp.docker.localdev`)";
            entryPoints = [ "web" ];
            service = "wp-service";
          };
          http.services.wp-service.loadBalancer.server.port = 80;
        };
      };

      services.phpfpm.pools.${app} = {
        user = app;
        settings = {
          "listen.owner" = "nginx";
          "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;
        };
        phpEnv."PATH" = lib.makeBinPath [ pkgs.php ];
      };

      services.mysql = {
        enable = true;
        package = pkgs.mariadb;
        settings = {
          "mysqld" = {
            "port" = 3308;
          };
        };
        initialScript =
          pkgs.writeText "initial-script" ''
            CREATE DATABASE IF NOT EXISTS wordpress;
            CREATE USER IF NOT EXISTS 'admin'@'localhost' IDENTIFIED BY 'password';
            GRANT ALL PRIVILEGES ON wordpress.* TO 'admin'@'localhost';
          '';

        ensureDatabases = [
          "wordpress"
        ];
        ensureUsers = [
          {
            name = "admin";
            ensurePermissions = {
              "admin.*" = "ALL PRIVILEGES";
              "*.*" = "ALL PRIVILEGES";
            };
          }
        ];
      };



      systemd.services.wordpress.serviceConfig = {
        ProtectSystem = lib.mkForce false;
        ProtectHome = lib.mkForce false;
        ReadWritePaths = [ "/var" "/home" "/home/www" ];
      };


      systemd.services.wpsetup = {
        path = with pkgs; [ coreutils wget gzip curl unzip rsync ];
        wantedBy = [ "multi-user.target" ];
        script = ''
         
          mkdir -p /var/www/wpdemo

          # WordPress
          cd  /var/www/wpdemo
          curl -L https://wordpress.org/latest.zip -o wordpress.zip 
          unzip wordpress.zip -d ./tmp
          mv ./tmp/*/* .
          rm -rf tmp
          rm ./wordpress.zip

          #  Storefront theme
          cd  /var/www/wpdemo/wp-content/themes
          curl -L https://downloads.wordpress.org/theme/storefront.4.2.0.zip -o storefront.zip
          unzip storefront.zip
          rm storefront.zip

          #  WooCommerce plugin
          cd  /var/www/wpdemo/wp-content/plugins
          curl -L https://downloads.wordpress.org/plugin/woocommerce.7.4.1.zip -o woocommerce.zip
          unzip woocommerce.zip
          rm woocommerce.zip
          chmod 777 -R  /var/www/wpdemo
        '';
        serviceConfig = {


          ProtectSystem = lib.mkForce false;
          ProtectHome = lib.mkForce false;
          ReadWritePaths = [ "/var" "/home" "/home/wpdemo" "/home/wpdemo/www" ];
        };
      };

      systemd.services.ngnix.serviceConfig = {

        ProtectSystem = lib.mkForce false;
        ProtectHome = lib.mkForce false;
        ReadWritePaths = [ "/var" "/home" "/home/wpdemo" "/home/wpdemo/www" ];
      };
      services.nginx = {
        enable = true;

        virtualHosts.${domain} = {
          listen = [{
            addr = "127.0.0.1";
            port = 80;
          }];
          serverName = "wp.docker.localdev";

          locations."/" = {
            root = "/var/www/wpdemo";
            extraConfig = ''
              access_log off;
              charset utf-8;
              etag off;
              index index.php;
              
              location ~ \.php$ {
                  fastcgi_split_path_info ^(.+\.php)(/.+)$;
                  fastcgi_pass  unix:${socket};
                  include ${pkgs.nginx}/conf/fastcgi_params;
                  include ${pkgs.nginx}/conf/fastcgi.conf;
              }
            '';
          };
        };
      };

      users.mutableUsers = true;

      users.users.${app} = {
        isSystemUser = true;
        createHome = true;
        home = "/home/wpdemo";
        group = app;
      };
      users.groups.${app} = { };
    };
    # bindMounts = {
    #   "/var/www/wpdemo/wp-content/themes" = {
    #     hostPath = "/var/www/wpdemo/wp-content/themes/";
    #     isReadOnly = false;
    #   };
    # };
  };

}

Summary:
Issue 1 & 2 - I replaced services.wordpress predefined in nix option with custom systemd.services.wpsetup . I could download and unpack zips to the correct location set up in nginx
Issue 3. MySQL service fixed with changing the port.
Issue 4 Sync is not solved yet, and i need it for editing themes on my local system.

     bindMounts = {
       "/var/www/wpdemo/wp-content/themes" = {
         hostPath = "/var/www/wpdemo/wp-content/themes/";
          isReadOnly = false;
        };
     };

it seems to prioritize the state of host directory. I mean this scenario:

[on host] directory "/var/www/wpdemo/wp-content/themes  is empty
[in container] initial shell script downloads and unpacks zip to “"/var/www/wpdemo/wp-content/themes”

Expected result : Directory themes on the host should be poulated with the contents of themes from the container.
Observed result : Directory themes on the host was empty at first and it deleted the contents of themes in the container as well as its parent wp-content.

Any ideas how to fix it?

1 Like

We’re going to need to look after this. ppl usually don’t host wordpresses on nixOS - I guess.