Tutorial for setting up the LAMP stack on a NixOS server?

It’s a bit tricky for evaluation error. You can use lib.traceVal and friends from nixpkgs to try and trace the value but usually I revert to carefully reading the code (which is unfair to nix beginners that don’t know how to read nix yet).

Here the issue is that source-code-derivation.nix with return { source-code = «derivation ...rivation»; }. The documentRoot wants a path or null but is getting an attrset. One way to fix this:

documentRoot = my-source.source-code;
1 Like

Thank you very much! :slight_smile: I’m now able to get the PHP files where I want them to be. I think that the next step is figuring out how to manage the database. I found this example project where they do this:

  services.mysql = {
    enable = true;
    package = pkgs.mariadb;
    initialDatabases = [
      { name = "goldenbook";
        schema = pkgs.writeText "init.sql" ''
          CREATE TABLE entries (text TEXT);
        '';
      }
    ];
    ensureUsers = [
      { name = "goldenuser";
        ensurePermissions = {
          "goldenbook.*" = "ALL PRIVILEGES";
        };
      }
    ];
  };

Is that a good way to get bootstrapped? Or do you have any other recommended examples/reading? Thanks again for the help so far!

Looks good to me! I would probably keep the schema in a DB migration tool and add some backup and restore on top of that.

1 Like

Keep in mind that initialDatabases only runs the first time your database starts… so if you already have a running database this won’t happen.

1 Like

for addition…like in my case i dont use mySql…i use microsoft SQL server on my project so i need to install php with custom extention…here is my config…

let
myPhp = pkgs.php.withExtensions ({ all, ... }: with all; [ sqlsrv pdo_sqlsrv pdo_odbc curl dom filter intl json mbstring tokenizer xmlwriter openssl ctype exif fileinfo gd gettext iconv pdo posix session]);
in {
  services.httpd = {
    enable = true;
    adminAddr = "webmaster@example.org";
    enablePHP = true;
    phpPackage = myPhp;
  };

   users.users.fadhli.extraGroups = [ "wwwrun" ];
   environment.unixODBCDrivers = with pkgs.unixODBCDrivers; [msodbcsql17];

i hope this can be useful… :slightly_smiling_face:

1 Like

Thank you all! These are my current files:

configuration.nix:

{ config, pkgs, ... }:
let
  my-source = import ./source-code-derivation.nix { inherit pkgs; };
  dbConfig = {
    db = "myDb";
    user = "myDbUser";
    password = "myDbPassword";
  };
in
{
  imports = [
    ./hardware-configuration.nix
  ];

  boot.cleanTmpDir = true;
  networking.hostName = "hostname";
  networking.firewall.allowPing = true;
  services.openssh.enable = true;
  users.users.root.openssh.authorizedKeys.keys = [
    "ssh-ed25519 mykey"
  ];

  environment.systemPackages = with pkgs; [
    neovim
  ];

  networking.firewall.allowedTCPPorts = [ 80 443 ];

  services.httpd.enable = true;
  services.httpd.adminAddr = "post@mysite";
  services.httpd.enablePHP = true; # oof... not a great idea in my opinion

  services.httpd.virtualHosts."example.org" = {
    documentRoot = my-source.source-code;
  };

  services.mysql = {
    enable = true;
    package = pkgs.mariadb;
    bind = "localhost";
    ensureDatabases = [
      dbConfig.db
    ];
    ensureUsers = [
      {
        name = "${dbConfig.user}";
        ensurePermissions = {
          "${dbConfig.db}.*" = "ALL PRIVILEGES";
        };
      }
    ];
  };
  # One-off systemd service to set the password for the database. Ref <https://joseph-long.com/writing/website-analytics-with-nixos/>
  systemd.services.setdbpass = {
    description = "MySQL database password setup";
    wants = [ "mysql.service" ];
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      ExecStart = ''
      ${pkgs.mariadb}/bin/mysql -e "grant all privileges on ${dbConfig.db}.* to ${dbConfig.user}@localhost identified by '${dbConfig.password}';" ${dbConfig.db}
      '';
      User = "root";
      PermissionsStartOnly = true;
      RemainAfterExit = true;
    };
  };

}

server.nix:

let
  nixos = import <nixpkgs/nixos> {
    configuration = import ./configuration.nix;
  };
in
  nixos.system

source-code-derivation.nix:

{ pkgs ? import <nixpkgs> {} }:

with pkgs;

{
  source-code = stdenv.mkDerivation {
    name = "source-code-php";

    src = /home/stian/path/to/code;

    phases = [ "installPhase" ];
    installPhase = ''
      cp -r $src $out
    '';
  };
}

I build and deploy like this:

  1. Build with nix-build server.nix. Note the output store path
  2. Copy to server with nix-copy-closure --to my_server_ip --use-substitutes path_from_step_one
  3. One the server:
    a. nix-env --profile /nix/var/nix/profiles/system --set path_from_step_one
    b. /nix/var/nix/profiles/system/bin/switch-to-configuration switch

I’m happy with this so far! Thanks for the help. Before I move on (to database migrations and lets encrypt), do you have any feedback to the above setup? Ex: The services.httpd.enablePHP = true; # oof... not a great idea in my opinion - what should I do instead?

One specific question: How can I make environment variables available to my PHP script? I’d like to set fex DBNAME=something and then use getenv('DBNAME') from PHP.

According Environment Variables in Apache - Apache HTTP Server Version 2.4 you should able to add this to the config file. Additional configuration can be set using services.httpd.extraConfig. You can do man configuration.nix to view all available options :slight_smile:

Thanks @jonringer! :slight_smile: This worked:

  services.httpd.virtualHosts."example.org" = {
    documentRoot = my-source.source-code;
    extraConfig = ''
      <Directory />
        DirectoryIndex index.php
        Require all granted
      </Directory>
      SetEnv MYSQL_HOST localhost
      SetEnv MYSQL_DATABASE ${dbConfig.db}
      SetEnv MYSQL_USER ${dbConfig.user}
      SetEnv MYSQL_PASSWORD ${dbConfig.password}
    '';
  };

Keep in mind this isn’t a good idea, though… as it exposes your password into the nix store which is world readable permission wise.

Given what you have previously posted you don’t actually need a password to authenticate to your database, you can use socket authentication instead.

If you want to have a working session on the LAMP with NixOS. It might be more efficient to handle various questions that could come up.

1 Like

Thanks again @aanderse ! I’ve successfully setup socket authentication. My configuration.nix now looks like this:

{ config, pkgs, ... }:
let
  my-source = import ./source-code-derivation.nix { inherit pkgs; };
  dbConfig = {
    db = "myDb";
    user = "myDbUser";
    socket = "'./run/mysqld/mysqld.sock'";
  };
in
{
  imports = [
    ./hardware-configuration.nix
  ];

  boot.cleanTmpDir = true;
  networking.hostName = "myhostname";
  networking.firewall.allowPing = true;
  services.openssh.enable = true;
  users.users.root.openssh.authorizedKeys.keys = [
    "ssh-ed25519 mykey myemail"
  ];

  environment.systemPackages = with pkgs; [
    neovim
  ];


  networking.firewall.allowedTCPPorts = [ 80 443 ];

  services.httpd.enable = true;
  services.httpd.adminAddr = "myemail";
  services.httpd.enablePHP = true; # oof... not a great idea in my opinion

  services.httpd.virtualHosts."example.org" = {
    documentRoot = my-source.source-code;
    extraConfig = ''
      <Directory />
        DirectoryIndex index.php
        Require all granted
      </Directory>
      SetEnv MYSQL_HOST localhost
      SetEnv MYSQL_DATABASE ${dbConfig.db}
      SetEnv MYSQL_USER ${dbConfig.user}
      SetEnv MYSQL_SOCKET ${dbConfig.socket}
    '';
  };

  services.mysql = {
    enable = true;
    package = pkgs.mariadb;
    bind = "localhost";
    ensureDatabases = [
      dbConfig.db
    ];
    ensureUsers = [
      {
        name = "${dbConfig.user}";
        ensurePermissions = {
          "${dbConfig.db}.*" = "ALL PRIVILEGES";
        };
      }
    ];
  };

}

Do you have any suggestions to decrease space usage in the nix store? I’d like this run this old app on a small VPS, and currently the nix store is taking about 4gb of space.

[root@extravmstianlagstad:/]# du /nix/ -sh
4.1G    /nix/

That’s after running nix-env -p /nix/var/nix/profiles/system --delete-generations +2 and nix-collect-garbage. Is it realistic/practical to decrease that 4gb?

1 Like

I would suggest opening a new topic for your space question. I have never tried to reduce disk space usage with NixOS because I only work on boxes where high disk space usage is expected. Sorry I couldn’t be helpful on this topic.

1 Like

Examine your system’s closure and see what is taking space.
nix path-info -rsSh /run/current-system.

For a NixOS container with your configuration (so without the “my-source” and hardware-configuration) i get a 1.4G closure size.

1 Like

Thanks again for responding, @aanderse !

Thanks @tomberek ! I did nix-build server.nix and then nix path-info -Sh ./result to see that the closure size is 1.6G. Not too bad, I think.

The next step for me is to setup Let’s Encrypt. I see that Nginx - NixOS Wiki has an example of the LEMP stack, so I’ll try using nginx instead of apache.

I think the letsencrypt part works, but I’m not able to load my site. When I go to the root domain, I get an nginx 403 error. When I explicitly go to /index.php, then I get a black page.

  • tail -f /var/log/nginx/access.log reports a 500
  • I don’t see anything in /var/log/nginx/error.log
  • I don’t see anything useful in journalctl -u phpfpm-mypool.service.

Any hints as to where I could look for the error log?

Current configuration.nix:

{ config, pkgs, ... }:
let
  my-source = import ./source-code-derivation.nix { inherit pkgs; };
  dbConfig = {
    db = "myDb";
    user = "myDbUser";
    socket = "'./run/mysqld/mysqld.sock'";
  };
in
{
  imports = [
    ./hardware-configuration.nix
  ];

  boot.cleanTmpDir = true;
  networking.hostName = "extravmexample";
  networking.firewall.allowPing = true;
  services.openssh.enable = true;
  users.users.root.openssh.authorizedKeys.keys = [
    "ssh-ed25519 *** test@example.com"
  ];

  environment.systemPackages = with pkgs; [
    neovim
  ];


  networking.firewall.allowedTCPPorts = [ 80 443 ];

  services.nginx = {
    enable = true;
    virtualHosts."example.com" = {
      enableACME = true;
      forceSSL = true;
      root = my-source.source-code;
      locations."~ \.php$".extraConfig = ''
        fastcgi_pass  unix:${config.services.phpfpm.pools.mypool.socket};
        fastcgi_index index.php;
      '';
    };
  };

  security.acme.certs = {
    "example.com".email = "test@example.com";
  };
  security.acme.acceptTerms = true;

  services.phpfpm.pools.mypool = {
    user = "nobody";
    settings = {
      pm = "dynamic";
      "listen.owner" = config.services.nginx.user;
      "pm.max_children" = 5;
      "pm.start_servers" = 2;
      "pm.min_spare_servers" = 1;
      "pm.max_spare_servers" = 3;
      "pm.max_requests" = 500;
      "catch_workers_output" = 1;
    };
  };

  services.mysql = {
    enable = true;
    package = pkgs.mariadb;
    bind = "localhost";
    ensureDatabases = [
      dbConfig.db
    ];
    ensureUsers = [
      {
        name = "${dbConfig.user}";
        ensurePermissions = {
          "${dbConfig.db}.*" = "ALL PRIVILEGES";
        };
      }
    ];
  };

}

Not sure if it’s worth mentioning, but I’m seeing this in /var/log/nginx/error.log:

2021/08/01 12:25:41 [warn] 38659#38659: could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size

Interesting way of using nix-shelk:

1 Like

It’s a modernized LAMP, but here’s how to setup a PHP project using https://devenv.sh

1 Like

You mean LCMP :face_with_hand_over_mouth:

This is really great @domenkozar :rocket: Having a full stack available for php developers is huge. I hope the devenv team keeps up all the great work!

1 Like

Is there a tutorial somewhere for setting up a PHP app with devenv? I’ve read a bit of devenv’s documentation and the PHP sample above but I’m afraid I need a lot more hand-holding. My main question is: where do I specify the file that is to be used as an entrypoint, e.g., index.php?

This seems like a good example that I assume would point at the base of the repository you included devenv in:

@shyim covered the entire process of php development in a great article as well: