Install · Manual setup

Configure nginx

This page writes by hand the nginx files that orionplace domain add creates: a site on port 80 for the certificate check, a Let's Encrypt certificate and the HTTPS site that passes requests to the storefront service. The examples use the domain shop.example.com and the storefront on port 3000.

How it works

nginx receives the public requests on ports 80 and 443. Port 80 only answers certificate checks and redirects to HTTPS. The HTTPS site passes everything to the storefront on 127.0.0.1 and tells it the domain and the customer's address in request headers. The new files sit next to your other sites and change none of them.

1 Requirements

You need nginx with HTTPS support, certbot and the running storefront service, see Run the storefront as a service. The domain's DNS A record, and AAAA if the server has IPv6, must point to this server, and ports 80 and 443 must be open in the server's firewall and in your cloud provider's firewall.

Check that nginx has HTTPS support and see which files and folders it includes:

bash
nginx -V 2>&1 | grep -o http_ssl_module
nginx -T 2>/dev/null | grep -E "^ *include"

Put the files into the folder that the http block includes: /etc/nginx/conf.d on Debian, Ubuntu, the RHEL family, Fedora, Amazon Linux and openSUSE, /etc/nginx/http.d on Alpine. The examples use conf.d.

Arch Linux includes no folder by default. Add this line inside the http block of /etc/nginx/nginx.conf:

nginx
    include /etc/nginx/conf.d/*.conf;

2 Shared settings

Once per server, at the http level: this map lets connections that ask for an upgrade (WebSocket) through to the storefront. If another of your files already has such a map under another name, you can use that name in the site instead.

/etc/nginx/conf.d/storefront-common.conf
map $http_upgrade $storefront_connection_upgrade {
    default upgrade;
    ''      close;
}

Long domain names can stop nginx with could not build server_names_hash. Then add this line once to the same file, unless another of your files sets it already: nginx refuses it twice.

nginx
server_names_hash_bucket_size 128;

3 Site on port 80

Create the folder certbot puts its check files into:

bash
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge

Then save the site file. It answers the certificate checks and shows a short notice until HTTPS is ready:

/etc/nginx/conf.d/shop.example.com.conf
server {
    listen 80;
    listen [::]:80;
    server_name shop.example.com www.shop.example.com;
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        default_type text/plain;
    }
    location / {
        default_type text/plain;
        return 503 "The storefront for shop.example.com is being set up.";
    }
}

Check the configuration and load it (on Alpine: sudo rc-service nginx reload):

bash
sudo nginx -t
sudo systemctl reload nginx

4 Certificate

Get one certificate for the domain and its www name. certbot puts its check files into the folder above, so nginx keeps running:

bash
sudo certbot certonly --webroot -w /var/www/letsencrypt \
  --cert-name shop.example.com -d shop.example.com -d www.shop.example.com \
  --agree-tos --email you@example.com

Certificates last 90 days. The certbot package renews them with a timer; on Alpine add a daily cron job that runs certbot renew --quiet. nginx must reload after a renewal to use the new certificate, so add a deploy hook (on Alpine with rc-service nginx reload), check the timer and test the renewal:

bash
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
printf '#!/bin/sh\nnginx -t && systemctl reload nginx\n' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
systemctl list-timers | grep -i certbot
sudo certbot renew --dry-run

5 HTTPS site

Replace the site file with the full version. Port 80 now redirects to HTTPS, www.shop.example.com redirects to shop.example.com, and the HTTPS site passes requests to the storefront:

/etc/nginx/conf.d/shop.example.com.conf
server {
    listen 80;
    listen [::]:80;
    server_name shop.example.com www.shop.example.com;
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        default_type text/plain;
    }
    location / {
        return 301 https://shop.example.com$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name www.shop.example.com;
    ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
    return 301 https://shop.example.com$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name shop.example.com;
    ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
    client_max_body_size 64m;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Front-Domain $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Init-Ip $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $storefront_connection_upgrade;
        proxy_buffering off;
        proxy_buffer_size 32k;
        proxy_buffers 8 32k;
        proxy_busy_buffers_size 64k;
        proxy_read_timeout 120s;
    }
}
LineWhat it does
Host, X-Forwarded-HostThe domain the customer opened.
X-Front-DomainThe storefront picks your personal backend by this domain. nginx always overwrites it with the requested domain, so a customer cannot send another value.
X-Forwarded-ProtoTells the storefront that the page was opened over HTTPS.
X-Real-IP, X-Forwarded-For, X-Init-IpThe customer's address; the storefront passes it on to your backend.
Upgrade, ConnectionLets connections that ask for an upgrade (WebSocket) through, with the map from Shared settings.
proxy_buffering offSends answers on to the customer as they arrive, without waiting for the whole page.
proxy_buffer_size 32kRoom for large response headers. A SvelteKit storefront sends its script preloads in one long header; with the default 4 KB nginx answers 502.
proxy_read_timeout 120sWaits up to 120 seconds for a slow answer.
client_max_body_size 64mAccepts uploads up to 64 MB, such as review photos and avatars; the nginx default is 1 MB.

nginx older than 1.25.1 does not know http2 on;: delete those lines and add http2 to the 443 listen lines instead. An nginx built without HTTP/2 gets neither.

nginx
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

6 Next to existing sites

Before loading the site, see which names and addresses your other sites use:

bash
sudo nginx -T 2>/dev/null | grep -nE "server_name|listen|# configuration file"

Each domain may be served by one site only. If another file lists shop.example.com or www.shop.example.com, nginx warns conflicting server name and uses only one of them: remove the name from one of the files. A wildcard name such as *.example.com in another site does not need a change, because exact names win over it.

If another site listens on a specific address, for example listen 203.0.113.5:443 ssl;, nginx sends that address's requests only to sites with the same listen line: add the line to these server blocks too. If another site has listen [::]:80 ipv6only=off;, delete listen 80; here, because that line already takes IPv4 as well.

7 Check

Check the configuration, load it and open the storefront:

bash
sudo nginx -t
sudo systemctl reload nginx
curl -I https://shop.example.com/

An answer with status 200 means the storefront is online. 502 means nginx does not reach the storefront: check the service and its port. 503 from the storefront means the domain is not added to the project.