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#
[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.targetWorkingDirectory matters: path.pwd points at it, and relative paths such as md-repo and public resolve from there.
systemctl daemon-reload
systemctl enable --now lexersite
systemctl status lexersite
journalctl -u lexersite -fnginx#
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:
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#
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#
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#
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#
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.