Custom lighttpd Configuration
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.conffile 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 likemy-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 incustom.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:
- Look at the error:
tail -n 30 ~/.lighttpd/error.log. - Move the offending file aside:
mv ~/.lighttpd/custom.d/my-rules.conf /tmp/. - Restart lighttpd (see above). Your site comes back on the working config.
- Fix the syntax in the moved-aside file, run the
-ttest, 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
- Web Hosting on Your Seedbox or Storage Box — the per-user web hosting feature this configures
- S3 Object Storage on PMSS — run an S3-compatible endpoint on your box
- Docker on PMSS — run containerized apps you can reverse-proxy from lighttpd
- Rclone — move files to and from your box