Jump to content

Custom lighttpd Configuration

From Pulsed Media Wiki


Every Pulsed Media seedbox and storage box runs a per-user lighttpd web server that serves ~/www/public/. You can extend that server's configuration yourself, without operator involvement, through a drop-in directory that PMSS builds into every generated config.

This page covers where your config goes, a quick reference for logs and restarts, which modules are available, and worked examples. It does not cover custom domains or DNS setup.

Quick reference

Task Command / path
Your config drop-in ~/.lighttpd/custom.d/*.conf
Generated (managed) config ~/.lighttpd.conf — do not edit; rebuilt on updates
Document root ~/www/public/
Follow the error log tail -f ~/.lighttpd/error.log
Follow the access log tail -f ~/.lighttpd/access.log
Is your lighttpd running? pgrep -u $(whoami) -x lighttpd
Test config before restarting /usr/sbin/lighttpd -t -f ~/.lighttpd.conf
Restart (apply changes) panel restart control, or killall -USR1 -u $(whoami) lighttpd
PHP settings ~/.lighttpd/php.ini

When something is not working, tail -f ~/.lighttpd/error.log in one terminal while you reload the page in a browser is the fastest way to see what lighttpd is complaining about.

Where your config goes

The generated per-user config ends with two include lines, so anything in either location is loaded on top of the defaults:

include "/home/USERNAME/.lighttpd/custom"
include "/home/USERNAME/.lighttpd/custom.d/*.conf"
  • ~/.lighttpd/custom.d/*.conf — the drop-in directory, and where your configuration belongs. Every .conf file in it is loaded. Files you create here survive PMSS upgrades. PMSS also writes its own managed fragments here (reverse proxies for Deluge, rclone, the media stack), so use distinct filenames like my-rules.conf, and do not delete files you did not create.
  • ~/.lighttpd/custom — a single legacy file that also loads, but it ships in the PMSS skel template and may be overwritten on upgrades. Deprecated. Put your config in custom.d/ instead.

Replace USERNAME with your own username throughout.

Which modules are available

lighttpd only understands a directive if the module that implements it is loaded. These are already loaded, so their directives work with no extra setup:

mod_access, mod_alias, mod_auth / mod_authn_file, mod_setenv, mod_proxy, mod_webdav, mod_fastcgi.

Anything else must be loaded first, at the top of your .conf file, before you use its directives:

server.modules += ( "mod_redirect" )

Common ones you may need to add this way: mod_redirect (redirects), mod_dirlisting (directory listings), mod_rewrite (URL rewriting).

Worked examples

Put each of these in a .conf file under ~/.lighttpd/custom.d/.

Response, caching, and CORS headers

mod_setenv is loaded, so headers need no extra module:

# Tell crawlers not to index this site
setenv.add-response-header = ( "X-Robots-Tag" => "noindex, nofollow" )

# Cache static assets for one hour
$HTTP["url"] =~ "\.(css|js|png|jpg|webp|woff2)$" {
    setenv.add-response-header = ( "Cache-Control" => "public, max-age=3600" )
}

# Allow cross-origin requests to an API path
$HTTP["url"] =~ "^/api" {
    setenv.add-response-header = ( "Access-Control-Allow-Origin" => "*" )
}

Reverse-proxy a path to a local app

Expose an app running on a local port (for example a Docker container) under a path of your site (mod_proxy, loaded):

$HTTP["url"] =~ "^/app" {
    proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => 8080 ) ) )
}

Password-protect a path

mod_auth is loaded. Create the password file with htpasswd -c ~/.lighttpd/mypasswd yourname:

auth.backend = "htpasswd"
auth.backend.htpasswd.userfile = "/home/USERNAME/.lighttpd/mypasswd"
auth.require = ( "/private" =>
    ( "method" => "basic", "realm" => "Private", "require" => "valid-user" )
)

Block access to files or paths

mod_access is loaded. Deny requests for sensitive names:

url.access-deny = ( "~", ".inc", ".git", ".env" )

Custom error page

Serve your own page instead of the default error (core directive, no module needed):

server.error-handler-404 = "/404.html"

Point it at /index.php instead if you run a single-page app that routes its own paths.

Directory listing

Show an index of files in folders that have no index file (mod_dirlisting, load it first):

server.modules += ( "mod_dirlisting" )
$HTTP["url"] =~ "^/files" {
    dir-listing.activate = "enable"
}

Redirect a path

server.modules += ( "mod_redirect" )
url.redirect = ( "^/old/(.*)" => "/new/$1" )

Clean URLs for a PHP application

Applications with a front controller (WordPress permalinks, Laravel, Slim) expect every request that is not a real file to route through index.php. lighttpd does this with mod_rewrite:

server.modules += ( "mod_rewrite" )
url.rewrite-if-not-file = ( "" => "/index.php${url.path}${qsa}" )

Existing files (images, CSS, uploads) are served directly; everything else goes to index.php with the original path and query string preserved. This replaces the Apache .htaccess rewrite rules those apps ship with.

Restrict a path to your own IP

Lock an admin area to a single address without a password (mod_access, loaded). Deny the path for every address except yours:

$HTTP["remoteip"] != "203.0.113.7" {
    $HTTP["url"] =~ "^/admin" { url.access-deny = ( "" ) }
}

An empty string in url.access-deny matches every request, so the block denies all of /admin unless the request comes from 203.0.113.7. Replace that with your own IP.

Block scraper bots by user-agent

Deny SEO crawlers and scrapers that ignore robots.txt:

$HTTP["useragent"] =~ "(?i)(ahrefsbot|semrushbot|mj12bot|dotbot)" {
    url.access-deny = ( "" )
}

The (?i) makes the match case-insensitive. Add or remove names as you like.

Hotlink protection

Stop other sites from embedding your images and spending your bandwidth. Deny image requests whose referer is neither empty (a direct visit) nor your own site:

$HTTP["referer"] !~ "^($|https?://([^/]+\.)?example\.com)" {
    $HTTP["url"] =~ "\.(jpg|jpeg|png|gif|webp)$" { url.access-deny = ( "" ) }
}

Replace example\.com with your own hostname (the backslash before the dot is required — it is a regular expression). Direct visits, where the browser sends no referer, are allowed.

Force files to download

By default the browser opens PDFs, images, and text in a tab. To make a folder always download instead, set Content-Disposition (mod_setenv, loaded):

$HTTP["url"] =~ "^/downloads/" {
    setenv.add-response-header = ( "Content-Disposition" => "attachment" )
}

Security headers

Add common protective headers to every response (mod_setenv, loaded):

setenv.add-response-header = (
    "X-Frame-Options" => "SAMEORIGIN",
    "X-Content-Type-Options" => "nosniff",
    "Referrer-Policy" => "no-referrer-when-downgrade"
)

X-Frame-Options blocks other sites from framing yours (clickjacking), and X-Content-Type-Options stops MIME-type guessing. HSTS is deliberately not included here — it only applies over HTTPS and is ignored on plain HTTP.

Maintenance mode

Send every visitor to a notice page while you work, without taking the site down (mod_redirect). Exclude the notice page itself so the redirect does not loop, and use a 302 so browsers do not cache it:

server.modules += ( "mod_redirect" )
url.redirect-code = 302
$HTTP["url"] !~ "^/maintenance\.html$" {
    url.redirect = ( "^/.*" => "/maintenance.html" )
}

The default redirect code is 301 (permanent), which browsers cache — visitors would stay stuck on the notice after you are done. 302 avoids that. Remove the block when maintenance is over.

Test before you restart

lighttpd reads its config only at start, so a change takes effect after a restart. Before restarting, check your config parses:

/usr/sbin/lighttpd -t -f ~/.lighttpd.conf

If it prints Syntax OK, restart your instance from the user panel (the restart control), or over SSH:

killall -USR1 -u $(whoami) lighttpd

Keep changes small and test after each one, so you always know which line broke a parse.

If your web server stops after a config change

A syntax error that slips past the test, or a bad directive at runtime, can stop lighttpd from starting, and your site returns an error until it is fixed. To recover:

  1. Look at the error: tail -n 30 ~/.lighttpd/error.log.
  2. Move the offending file aside: mv ~/.lighttpd/custom.d/my-rules.conf /tmp/.
  3. Restart lighttpd (see above). Your site comes back on the working config.
  4. Fix the syntax in the moved-aside file, run the -t test, and put it back.

The lighttpd manual (Configuration Options, Docs index) documents every directive.

What you should not change

The generated parts of the config (document root, per-user port, logging, PHP handler) are managed by PMSS in ~/.lighttpd.conf and rebuilt on updates. Put your changes only in custom.d/; edits made to the generated config are overwritten. Do not point the document root away from ~/www/public/, and do not remove the PMSS-managed files in custom.d/.

See Also