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

I’d like to move an old PHP application that I have from a Ubuntu server to a NixOS server, but I’m struggling a bit to get started. There are many great guides online for setting up the LAMP stack on Ubuntu (example), but I haven’t found any step by step guides showing the same with NixOS. I’m hoping something has escaped my googling: Are there any good resources out there?

Are you looking for like-to-like, or to “improve” things a bit?

Yes. A tutorial, or a template that I can fit to my needs (and perhaps I can write a tutorial myself from that).

Not many people use the LAMP stack on NixOS. We don’t have many apache fans here, most people use nginx. Anyways, here is the most basic LAMP setup you can get up and running with on NixOS:

{ config, pkgs, lib, ... }:
{
  networking.firewall.allowedTCPPorts = [ 80 443 ];

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

  services.httpd.virtualHosts."example.org" = {
    documentRoot = "/var/www/example.org";
    # want ssl + a let's encrypt certificate? add `forceSSL = true;` right here
  };

  services.mysql.enable = true;
  services.mysql.package = pkgs.mariadb;

  # hacky way to create our directory structure and index page... don't actually use this
  systemd.tmpfiles.rules = [
    "d /var/www/example.org"
    "f /var/www/example.org/index.php - - - - <?php phpinfo();"
  ];
}

You’ll want to take a quick glance at this guide if you want free ssl certificates.

Your question is broad and general, so feel free to ask specific questions. I do a ton of LAMP sysadmin work on NixOS, so hopefully I’ll have an answer for you.

6 Likes

Thank you very much, @aanderse ! Now I have PHP working (I see the phpinfo as expected). This is my setup thus far:

server.nix

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

configuration.nix

{ config, pkgs, ... }: {
  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"
  ];

  environment.systemPackages = with pkgs; [
    neovim
  ];

  networking.firewall.allowedTCPPorts = [ 80 443 ];

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

  services.httpd.virtualHosts."example.org" = {
    documentRoot = "/var/www/mysite.com";
    # want ssl + a let's encrypt certificate? add `forceSSL = true;` right here
  };

  services.mysql.enable = true;
  services.mysql.package = pkgs.mariadb;

  # hacky way to create our directory structure and index page... don't actually use this
  systemd.tmpfiles.rules = [
    "d /var/www/mysite.com"
    "f /var/www/mysite.com/index.php - - - - <?php phpinfo();"
  ];
}

And I deploy like this:

  1. nix-build server.nix --no-out-link | nix-copy-closure -v --to --use-substitutes root@mysite.com
  2. sudo nix-env --profile /nix/var/nix/profiles/system --set path_from_nix_build_above
    sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch
    

(I got this way of doing it from this excellent post: Deploying NixOS to Amazon EC2)

The next step is to figure out a good way to package my application and deploy that at the same time. Do you have any recommended reading or tips for me that could guide me? Thanks again.

I would need a few more details about your application like what framework it is written in, etc…

A good start might be looking up composer2nix.

Thanks for responding! No frameworks at all, just a bunch of PHP files :slight_smile: Not using composer (no external dependencies).

In that case you could create a simple derivation pointing to your source code and set it to your directory root in apache.

Have you written a derivation before or would you need a hand with that?

Once you have your derivation you should be able to change the documentRoot in your virtual host to your derivation and that should cover it.

If your application uses a database, writes to a local file system, has secret data, etc… then you need to decide how to configure your application as well. I would need to know the details on how you supply database credentials to your application, etc, before I could make suggestions on that. Is your the source code to your web application public so I could look and make a few suggestions?

2 Likes

Would a derivation like this work for the source code?

with import <nixpkgs> {};

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

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

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

When I build that with nix-build source-derivation.nix that seems to work, and I get a result pointing to a path in the store. How can I use this derivation within my configuration.nix? Do I import it like this?

{ config, pkgs, ... }: {
  imports = [
    ./hardware-configuration.nix
    ./source-derivation.nix
  ];
...

If so, how do I specify that the apache documentroot should point to the result of that derivation?

Thanks again for the help so far!

One way to do it is like this:

{ config, pkgs, ... }:
let
  # Get a reference to the source.
  my-source = import ./source-derivation.nix { inherit pkgs; };
in
{
  imports = [
    ./hardware-configuration.nix
  ];
  # ...

  services.httpd.virtualHosts."example.org" = {
    # pass is as the document root
    documentRoot = my-source;
  };
}

NOTE: The imports are useful to split the NixOS configuration, which work differently.

And update the source so that the pkgs instance is optional and can be passed around:

{ pkgs ? import <nixpkgs> {} }:
with pkgs;
# ...
1 Like

Thanks! I now have this in configuration.nix:

{ config, pkgs, ... }:
let
  my-source = import ./source-code-derivation.nix { inherit pkgs; };
in
{
  imports = [
    ./hardware-configuration.nix
  ];

...

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

...

and this in 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
    '';
  };
}

When I try to build now i get an error:

$ nix-build server.nix --show-trace
error: while evaluating the attribute 'system' at /nix/var/nix/profiles/per-user/root/channels/nixos/nixos/default.nix:38:3:
while evaluating the attribute 'config.system.build.toplevel' at /nix/var/nix/profiles/per-user/root/channels/nixos/nixos/modules/system/activation/top-level.nix:293:5:
while evaluating 'foldr' at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/lists.nix:52:20, called from /nix/var/nix/profiles/pe

....

while evaluating the attribute 'documentRoot' at undefined position:
while evaluating 'g' at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/attrsets.nix:276:19, called from undefined position:
while evaluating anonymous function at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:98:72, called from /nix/var/nix/profiles/per-user/root/channels/nixos/lib/attrsets.nix:279:20:
while evaluating the attribute 'value' at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:465:9:
while evaluating the option `services.httpd.virtualHosts.example.org.documentRoot':
while evaluating the attribute 'mergedValue' at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:497:5:
The option value `services.httpd.virtualHosts.example.org.documentRoot' in `<unknown-file>' is not of type `null or path'.

Any way for me to debug what my-source actually is at that point?

I think I figured that one out. My source code derivation is returning an attribute set, so I had to do this instead (in configuration.nix):

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

That is my-source.source-code instead of just my-source.

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: