Skip to content
LexerLangv0.1.0

nginx and systemd

Reverse proxy, content-type fixes, a hardened service unit and deploying.

The application runs on a port on 127.0.0.1; TLS, security headers and static files live in nginx.

The systemd unit#

.env
[Unit]
Description=LexerLang Documentation (LexerLang)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/var/www/lexer
Environment=HOME=/var/www/lexer
ExecStart=/usr/local/bin/lexer serve /var/www/lexer/app.lexer --production
Restart=always
RestartSec=3

StandardOutput=journal
StandardError=journal
SyslogIdentifier=lexersite

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/lexer
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true

[Install]
WantedBy=multi-user.target

WorkingDirectory matters: path.pwd points at it, and relative paths such as md-repo and public resolve from there.

Terminal
systemctl daemon-reload
systemctl enable --now lexersite
systemctl status lexersite
journalctl -u lexersite -f

nginx#

nginx
upstream lexersite_app {
    server 127.0.0.1:3500;
    keepalive 16;
}

server {
    listen 443 ssl;
    http2 on;
    server_name lexer.gurerlabs.com;

    ssl_certificate     /etc/letsencrypt/live/lexer.gurerlabs.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/lexer.gurerlabs.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header Referrer-Policy           "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://lexersite_app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 30s;
    }
}

Fixing the content type#

$res.send() sets the Content-Type to text/html in every case. For addresses that return XML or plain text, nginx corrects it:

nginx
location = /sitemap.xml {
    proxy_pass http://lexersite_app;
    proxy_set_header Host $host;
    proxy_hide_header Content-Type;
    add_header Content-Type "application/xml; charset=utf-8" always;
    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Content-Type-Options "nosniff" always;
}

proxy_hide_header removes what came from upstream and add_header puts the right one in place. Both are needed together; add_header alone would produce two Content-Type headers.

Static files#

nginx
location /public/ {
    alias /var/www/lexer/public/;
    access_log off;
    expires 1h;

    gzip on;
    gzip_vary on;
    gzip_min_length 512;
    gzip_types text/css application/javascript image/svg+xml;

    add_header Cache-Control "public, must-revalidate";
    try_files $uri =404;
}

nginx's global gzip is often off, which sends CSS and JS uncompressed straight from disk. Turning it on inside this block alone is enough.

Files requested from the root#

nginx
location = /favicon.ico {
    alias /var/www/lexer/public/favicon/favicon.ico;
    access_log off;
    expires 7d;
}

location = /site.webmanifest {
    alias /var/www/lexer/public/site.webmanifest;
    default_type application/manifest+json;
    expires 1d;
}

default_type is required: nginx does not recognise the .webmanifest extension and sends the file as application/octet-stream.

Certificates#

Terminal
certbot certonly --webroot -w /var/www/html -d lexer.gurerlabs.com

--webroot rather than --nginx: the --nginx plugin edits conf files itself and can disturb hand-written blocks.

Deploying#

Terminal
rsync -az --delete \
    --exclude='.lexer/' --exclude='.logs/' \
    ./ server:/var/www/lexer/

ssh server systemctl restart lexersite

.lexer/ and .logs/ must be excluded: the first is the cache directory generated on the server, the second the runtime logs. Without listing them, --delete would wipe both on every deploy.

The end#

That is the end of the documentation. If something is missing, lexerlang@gurerlabs.com.