Setting Up a Production Nginx Server on NixOS

In previous posts, I’ve mentioned that I maintain a stack using NixOS. This website you’re reading is included. The gateway from my backend to the world is Nginx. I’m going to walk through how I set up Nginx on NixOS for this setup so you can avoid some of the pitfalls I ran into.

Background

Nginx is a rock-solid web server that provides a ton of features out of the box. NixOS provides the Nginx service via its Nix packages. If you’re coming from working with Nginx configs like me, it might be a bit confusing and frankly frustrating to set up Nginx.

I initially thought that you can just bring your own Nginx configuration file and point the NixOS settings to that. This is not the case. The configurations from a regular Nginx file need to be converted to NixOS configuration syntax.

However, once you wrap your head around the configurations exposed, it all makes sense. Making updates is super easy, no having to FTP a file to the server.

Setting Up Our Webserver

We break this down into a few steps. We have a basic config to walk through the Nginx server, then we delve into TLS certs and subdomains to round it out. Jump to the end of the article if you want to just see the complete configuration.

Basic Web Server

We’re going to create the most basic Nginx we server. This will only serve HTTP traffic and will send a static message for a single domain.

  1. Enabling The Webserver

    We need to enable our Nginx web server. It’s built into Nix Packages, so we can just go ahead and enable it.

    services.nginx = {
        enable = true;
    };
    
  2. Creating our Virtual Host

    We can go ahead and define a virtual host that will serve our domain. In this case, it’s static and will return a 200 response for every incoming request.

    services.nginx = {
        enable = true;
        virtualHosts = {
            "mywebsite.com" = {
                serverName = "mywebsite.com";
                forceSSL = false;
                locations."/" = {
                    return = "200 \"Hello from mywebsite!\"";
                };
            };
        };
    };
    
  3. Opening Up the Port

    Lastly, we need to open up the port on our host. Since it’s an HTTP server, we open up port 80.

    networking.firewall.allowedTCPPorts = [
        80
    ];
    

Adding TLS Encryption

Websites and web serverices these days are pretty much useless if they do not support HTTPS traffic. We can get a cert for our domain and start serving encrypted traffic.

  1. Create our acme user

    Certs work by granting the Nginx user access to the acme group.

    users.users.nginx.extraGroups = [ "acme" ];
    
  2. Define a certificate Using security.acme, we can go ahead and define our cert.

    security.acme = {
        acceptTerms = true;
        defaults.email = "burak@mywebsite.com";
        # Staging server for testing configurations....
        # defaults.server = "https://acme-staging-v02.api.letsencrypt.org/directory";
        certs = {
            "mywebsite.com" = {
                webroot = "/var/lib/acme/challenges-mywebsite";
                email = "burak@mywebsite.com";
                group = "nginx";
            };
        };
    };
    
  3. Wire Up to Our Virtual Hosts

    We now go ahead and add our certificate to our virtual host configuration.

    services.nginx = {
        enable = true;
        virtualHosts = {
            "mywebsite.com" = {
                serverName = "mywebsite.com";
                useACMEHost = "mywebsite.com";
                acmeRoot = "/var/lib/acme/challenges-mywebsite";
                addSSL = true;
                forceSSL = false;
                locations."/" = {
                    return = "200 \"Hello from mywebsite!\"";
                };
            };
        };
    };
    
  4. Opening Up the Port

    HTTPS requires a different port, so let’s go ahead and open that up.

    networking.firewall.allowedTCPPorts = [
        80
        443
    ];
    
  5. Forcing SSL

    The last thing that I like to do is enable the forceSSL option. This means that all traffic will be redirected to the HTTPS.

    services.nginx = {
        enable = true;
        virtualHosts = {
            "mywebsite.com" = {
                serverName = "mywebsite.com";
                useACMEHost = "mywebsite.com";
                acmeRoot = "/var/lib/acme/challenges-mywebsite";
                forceSSL = true; #<-- Change here
                locations."/" = {
                    return = "200 \"Hello from mywebsite!\"";
                };
            };
        };
    };
    

Adding Subdomains

Another big use case that I use is using subdomains with the main domain. There are a few gotchas, especially around the TLS certificate. We’re going to walk through the steps to add a subdomain.

  1. Adding a Subdomain Virtual Host

    Create a virtual host similar to above. In this case, we’re reverse proxying a service running locally on port 7000.

    services.nginx = {
        enable = true;
        virtualHosts = {
            "mywebsite.com" = {
                ...
            };
            "subdomain.mywebsite.com" = {
                serverName = "registry.mywebsite.com";
                forceSSL = false;
                locations."/" = {
                    recommendedProxySettings = true;
                    proxyPass = "http://127.0.0.1:7000";
                };
            };
        };
    };
    
  2. Adding Subdomain to the Certificate

    Since it’s a subdomain, both vhosts can use the same cert. We can modify our existing cert by adding the configuration extraDomainNames to include this subdomain (and others) onto the same cert.

    security.acme = {
        acceptTerms = true;
        defaults.email = "burak@mywebsite.com";
        certs = {
            "mywebsite.com" = {
                webroot = "/var/lib/acme/challenges-mywebsite";
                email = "burak@mywebsite.com";
                group = "nginx";
                extraDomainNames = [
                    "subdomain.mywebsite.com"
                ];
            };
        };
    };
    
  3. Wiring Up Subdomain to Cert This is very similar to our first virtual host. Remember we’re using the same certificate as the root domain. So the certificate configs will be the same as our root Vhost.

    services.nginx = {
        enable = true;
        virtualHosts = {
            "mywebsite.com" = {
                ...
            };
            "subdomain.mywebsite.com" = {
                serverName = "registry.mywebsite.com";
                useACMEHost = "mywebsite.com";
                addSSL = true;
                forceSSL = false;
                acmeRoot = "/var/lib/acme/challenges-mywebsite";
                locations."/" = {
                    recommendedProxySettings = true;
                    proxyPass = "http://127.0.0.1:7000";
                };
            };
        };
    };
    

Bonus Fun Configuration

One thing I like to do is define a default route. This is critical when working with multiple domains. This prevents issues with domains that are pointed here via DNS but not defined from causing problems. This is because the default branch catches these interactions that are unintended.

services.nginx = {
    enable = true;

    virtualHosts = {
        default = {
            serverName = "_";
            default = true;
            rejectSSL = true;
            locations."/".return = "444";
        };
        "mywebsite.com" = {
            ...
        };
        "subdomain.mywebsite.com" = {
            ...
        };
    };
};

The Final Configuration

Putting all the pieces together, here is what our final configuration looks like:

{
    # Enable the docker registry
    networking.firewall.allowedTCPPorts = [
        80
        443
    ];

    security.acme = {
        acceptTerms = true;
        defaults.email = "burak@mywebsite.com";
        certs = {
            "mywebsite.com" = {
                webroot = "/var/lib/acme/challenges-mywebsite";
                email = "burak@mywebsite.com";
                group = "nginx";
                extraDomainNames = [
                    "subdomain.mywebsite.com"
                ];
            };
        };
    };
    users.users.nginx.extraGroups = [ "acme" ];

    services.nginx = {
        enable = true;
        virtualHosts = {
            default = {
                serverName = "_";
                default = true;
                rejectSSL = true;
                locations."/".return = "444";
            };
            "mywebsite.com" = {
                serverName = "mywebsite.com";
                useACMEHost = "mywebsite.com";
                acmeRoot = "/var/lib/acme/challenges-mywebsite";
                forceSSL = true;
                locations."/" = {
                    return = "200 \"Hello from mywebsite!\"";
                };
            };
            "subdomain.mywebsite.com" = {
                serverName = "registry.mywebsite.com";
                useACMEHost = "mywebsite.com";
                forceSSL = true;
                acmeRoot = "/var/lib/acme/challenges-mywebsite";
                locations."/" = {
                    recommendedProxySettings = true;
                    proxyPass = "http://127.0.0.1:7000";
                };
            };
        };
    };
}

What’s Next

Now that you’ve got your web server configured, you can go ahead and start building out the rest of your applications. I have written extensively about running containers and setting up continuous deployment on NixOS. Hopefully, I have helped you take the leap into the NixOS world or improved your NixOS setup.

More sweet content?

Stay up to date!

I don't sell your data. Read my privacy policy .

Related Articles