Jump to content

Lighttpd Custom Configuration

From Pulsed Media Wiki

Lighttpd Custom Configuration

Every user on a PMSS server runs their own lighttpd instance under their own UID. You can extend its configuration with custom fragments — reverse proxies for Docker containers, URL rewrites, custom headers, or anything lighttpd supports.

The custom.d/ Directory

The recommended location for custom configuration is:

~/.lighttpd/custom.d/

This directory is created automatically when your account is provisioned. Every *.conf file inside it is included by your lighttpd instance at startup.

To add configuration, create a new .conf file:

nano ~/.lighttpd/custom.d/myapp.conf

Save the file; lighttpd picks it up within about a minute. See Applying Your Changes below.

PMSS-Managed Files

Some *.conf files in custom.d/ are managed by PMSS and will be overwritten on regeneration. Do not edit these:

  • pmss-rclone.conf
  • pmss-qbittorrent.conf
  • pmss-deluge.conf
  • pmss-invidious.conf

Any other filename is yours. Use descriptive names: jellyfin.conf, syncthing.conf, myapp-proxy.conf.

Applying Your Changes

Saving the file is all a current Pulsed Media server needs. A check runs every minute and restarts your lighttpd when ~/.lighttpd.conf, ~/.lighttpd/custom or any ~/.lighttpd/custom.d/*.conf file is newer than the running lighttpd process. The new configuration is live within about a minute. Your web panel and proxied apps are briefly unavailable while lighttpd restarts.

Check the syntax right after saving, before the restart picks the file up:

lighttpd -t -f ~/.lighttpd.conf

A syntax error keeps lighttpd from starting, and your web panel stays down until the file is fixed. See Troubleshooting.

To restart on demand without changing a setting, update the timestamp of the legacy file with touch ~/.lighttpd/custom. The next check, within about a minute, restarts lighttpd.

If a saved change has still not taken effect after a few minutes, the automatic check may not be running on your server. Open a support ticket rather than restarting repeatedly.

Deleting or disabling a file

The check compares the modification times of the files that exist. Deleting a .conf file, or renaming it to myapp.conf.disabled, changes no remaining file, so no restart happens and lighttpd keeps serving the old configuration. Renaming it back to .conf keeps its old modification time, which is usually older than the running lighttpd, so that is not picked up either. After any of these, update the timestamp of the legacy file so the next check restarts lighttpd:

touch ~/.lighttpd/custom

Why not kill -HUP?

kill -HUP does not reload lighttpd's configuration. lighttpd treats SIGHUP as a log rotation signal: it reopens its log files and writes logfiles cycled to ~/.lighttpd/error.log. The configuration loaded at startup stays in use. Earlier versions of this page recommended kill -HUP for reloading; that was wrong. To confirm a change is actually live, see Applying Your Changes.

Reverse Proxy Template

The most common use case is proxying a Docker container or local application through your lighttpd instance. Here is a working template:

$HTTP["url"] =~ "^/user-USERNAME/myapp/" {
  auth.require = ()
  proxy.server = ( "" => ( (
    "host" => "127.0.0.1",
    "port" => 8080
  ) ) ),
  proxy.header = (
    "upgrade" => "enable",
    "map-urlpath" => (
      "/user-USERNAME/myapp/" => "/",
      "/user-USERNAME/myapp"  => ""
    )
  )
}

Replace USERNAME with your actual username and 8080 with the port your application listens on.

What Each Part Does

auth.require = ()

This is critical. Your lighttpd instance has global authentication that protects the /user-USERNAME/ path. Without auth.require = (), lighttpd authenticates the request and consumes the Authorization header. If your backend application also expects authentication, it never receives the credentials — the user sees a permanent login loop.

Setting auth.require = () on the proxied path tells lighttpd: "skip authentication for this URL block; let the backend handle it." Your main panel login still protects everything else.

Use it only for an application that has its own login, and turn that login on first. Leaving the line out keeps your panel login in front of the application's web address: a browser without your panel credentials gets a 401 error. The panel login does not cover the port itself. Other accounts on the same server can connect to 127.0.0.1 directly, so an application without a login of its own is open to them.

map-urlpath

Your application expects requests at /, but users access it at /user-USERNAME/myapp/. The map-urlpath directive strips the prefix before forwarding the request to the backend.

Without this, your application receives the full path (/user-USERNAME/myapp/page) and cannot match its routes.

Two entries are needed: one with a trailing slash, one without. This handles both /user-USERNAME/myapp/ and /user-USERNAME/myapp.

Some applications have a base URL setting (often called URL base or base path). For those, set it to /user-USERNAME/myapp and remove the map-urlpath block: the application then receives the full path and builds its links under it. Self-Hosted Apps on PMSS lists the setting name for many applications.

"upgrade" => "enable"

Lets WebSocket connections through to the application. Many web applications use WebSockets for live updates, media players and terminals. Without this line their pages load, but those parts never connect. It changes nothing for an application that does not use WebSockets.

Choosing a Port

Your application (Docker container or otherwise) must listen on a port that isn't already in use. Bind to 127.0.0.1 so it is not reachable directly from the internet. Other accounts on the same server can still connect to it, so the application needs its own login.

Pick any unused port above 1024. Check what's already in use:

ss -tlnp

PMSS allocates service ports randomly in the 2000-38000 range. Before binding to any port, check what is already in use with ss -tlnp above.

For Docker containers, bind the port explicitly to loopback:

docker run -d --name myapp -p 127.0.0.1:8080:80 myimage

Example: Proxying a Docker Container

Full walkthrough: running a web application in Docker and making it accessible through your panel.

1. Start the container:

docker run -d --name filebrowser \
  -p 127.0.0.1:9090:80 \
  -v ~/data:/srv \
  filebrowser/filebrowser

2. Create the proxy configuration:

cat > ~/.lighttpd/custom.d/filebrowser.conf << 'EOF'
$HTTP["url"] =~ "^/user-johndoe/filebrowser/" {
  auth.require = ()
  proxy.server = ( "" => ( (
    "host" => "127.0.0.1",
    "port" => 9090
  ) ) ),
  proxy.header = (
    "upgrade" => "enable",
    "map-urlpath" => (
      "/user-johndoe/filebrowser/" => "/",
      "/user-johndoe/filebrowser"  => ""
    )
  )
}
EOF

3. Check the syntax, then wait about a minute for lighttpd to restart with the new file:

lighttpd -t -f ~/.lighttpd.conf

4. Access it:

Visit https://SERVER.pulsedmedia.com/user-johndoe/filebrowser/

5. (Optional) Add a tab:

Add to ~/.customFrames (see Custom Tabs):

filebrowser|Web File Browser|Files|/user-johndoe/filebrowser/

Legacy: ~/.lighttpd/custom

The single file ~/.lighttpd/custom (no directory, no extension) is also included by lighttpd. This is a legacy mechanism that still works. However, custom.d/ is preferred because:

  • You can organize configuration into separate files per application.
  • You can enable/disable an application by renaming its file (e.g., append .disabled), then restarting lighttpd as described in Deleting or disabling a file.
  • PMSS manages its own fragments in custom.d/ without touching your files.

Important: The ~/.lighttpd/custom file must exist if your lighttpd configuration references it. If it's missing, lighttpd will fail to start. If you don't need it, keep it as an empty file:

touch ~/.lighttpd/custom

Troubleshooting

lighttpd won't start after config change

Check the error log:

cat ~/.lighttpd/error.log

Common causes:

  • Syntax error in your .conf file — lighttpd's error message will point to the line.
  • Missing ~/.lighttpd/custom file (see Legacy section above).

Test your configuration:

lighttpd -t -f ~/.lighttpd.conf

Proxy returns 502 Bad Gateway

The backend application isn't running or isn't listening on the port you specified. Check:

ss -tlnp | grep PORT
docker ps

Permanent login loop (401 errors)

If the application has its own login, you forgot auth.require = () in your proxy block. Without it, lighttpd's global authentication consumes the credentials before they reach your backend. Add the empty auth directive and save; the change applies within about a minute.

Application works directly but not through the proxy

Test direct access:

curl http://127.0.0.1:PORT/

If that works but the proxied URL doesn't, check:

  • Is lighttpd actually using your config? See Applying Your Changes.
  • Is map-urlpath correct? The application must see /, not the full proxied path, unless you set its base URL to that path and removed map-urlpath.
  • Does the page load but a player, live view or terminal never connects? Add "upgrade" => "enable" to proxy.header.

See Also

  • Custom Tabs — adding tabs to your web panel for proxied applications
  • Docker Rootless — running containers on your seedbox
  • Invidious — PMSS-integrated private YouTube frontend (auto-configured proxy)
  • Install Media Stack — one-command Jellyfin/Sonarr/Radarr installation