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
  • pmss-transmission.conf
  • pmss-<app>.conf — one for each app you enable in Configure (today: slskd, copyparty, qui, Scryer)
  • media-stack.conf — written by the media-stack installer
  • custom-migrated.conf — written by the media-stack installer when it moves an older ~/.lighttpd/custom in

Every pmss-<app>.conf name is reserved, including apps added later, so never give your own file that shape.

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:

/usr/sbin/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 = ()
  setenv.set-request-header = ( "Authorization" => "" )
  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 asks for your panel login on the proxied path as well. An application that uses HTTP basic auth itself shares the one Authorization header with your panel login, so it never receives its own credentials — the user sees a permanent login loop. An application with a login page asks for its own login after the panel's.

Setting auth.require = () on the proxied path tells lighttpd: "skip authentication for this URL block; let the backend handle it." lighttpd then passes the browser's Authorization header on to the backend. After you log into your panel, your browser sends your panel login, which is your account password, in that header with every request under /user-USERNAME/, so without the next line the application receives your password. 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.

Without auth.require = () your panel login still reaches the application: once lighttpd has accepted it, it passes the header on to the backend as well. Keep the setenv.set-request-header line in that fragment too:

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

setenv.set-request-header

Removes the Authorization header before the request reaches the application, so your panel login stays with your web server. Keep it in every proxy fragment, with or without auth.require = (); in a fragment that keeps the panel login, the login still works and the application receives nothing. Applications with their own login page, a form with user name and password fields, which is most of them, do not read that header. Leave it out for an application that uses HTTP basic auth itself, where the browser shows a sign-in popup instead of a page. The line would remove that application's own login as well, and the login would loop. Such an application receives your panel login from a browser that is logged into your panel, until you sign in to the application; open it in a separate browser profile to keep your panel password away from it. Webtop is one such application.

Some applications with a login page send an Authorization: Bearer token of their own in the same header once you sign in. The application's guide says so; Ombi and Cleanuparr are two of them. The line above removes that token as well: in the Ombi test, sign-in succeeded and every request after it was refused. Such an application needs auth.require = () in its $HTTP["url"] block, because your panel login refuses a request that carries only the token. Put the line inside a condition within that block, so that it removes only your panel login, which the browser sends as Authorization: Basic:

$REQUEST_HEADER["Authorization"] =~ "^Basic " {
  setenv.set-request-header = ( "Authorization" => "" )
}

The condition needs lighttpd 1.4.46 or newer; /usr/sbin/lighttpd -v shows yours. For every other application, keep the plain line.

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 = ()
  setenv.set-request-header = ( "Authorization" => "" )
  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:

/usr/sbin/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

Normal PMSS updates never change this file. One thing does: when you run the media-stack installer, if ~/.lighttpd/custom has content and custom.d/media-stack.conf does not exist yet, the installer copies the file to ~/.lighttpd/backup.custom.YYYYMMDD and moves its rules to custom.d/custom-migrated.conf, leaving ~/.lighttpd/custom empty. Your own rules keep loading from that new file.

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:

/usr/sbin/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 uses HTTP basic auth itself, you forgot auth.require = () in your proxy block. Without it, your panel login and the application's login compete for the same Authorization header. Add the empty auth directive and save; the change applies within about a minute. An application that uses HTTP basic auth itself also loops if the block has the setenv.set-request-header line; remove that line for it.

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. A page that loads blank is covered in The page loads but stays blank.
  • Does the page load but a player, live view or terminal never connects? Add "upgrade" => "enable" to proxy.header.

The page loads but stays blank

The address answers without an error page, and the browser tab may even show the application's name, but the page itself is empty, white or without any styling.

One cause is an application that builds the links to its scripts and stylesheets from the server root: its page contains <base href="/">, or links that start with a root path such as /static/ or /assets/. Behind map-urlpath the application believes it lives at /, so the browser requests those files from https://SERVER.pulsedmedia.com/static/.... That address is outside your account, and the server answers it with 404 Not Found.

To check, open the browser's developer tools, select the Network tab and reload the page. If the failed .js or .css requests have addresses that do not start with /user-USERNAME/myapp/, this is the cause. If the failing requests do start with that path, or nothing fails, the cause is something else, and the application's own log is the next place to look.

To fix it:

  1. Set the application's base URL to /user-USERNAME/myapp. While the page is blank you usually cannot reach the application's settings page, so change it in its configuration file or start options. In NZBHydra2, for example, it is urlBase under main: in nzbhydra.yml, which defaults to /. In File Browser, used in the example above, it is the --baseURL option. Self-Hosted Apps on PMSS lists the setting name for many more applications, and the exact form each one needs (a few want the full address or a trailing slash).
  2. In the proxy file, delete the "map-urlpath" => ( ... ) entry and the comma before it, so proxy.header keeps only "upgrade" => "enable". The application then receives the full path (see map-urlpath). lighttpd applies the changed file within about a minute.
  3. Make the application pick up its new setting. For a setting in its own configuration file, stop the application, edit the file, then start it again. A Docker command-line option or a -e environment variable is fixed when the container is created, so the container has to be recreated. Before you remove it, run docker inspect NAME to see which folders are mounted, copy anything you need from the rest with docker cp, and keep your original docker run command: only mounted folders keep their files. Then run docker rm -f NAME and the full docker run command again, with the new option.
  4. Wait about a minute, reload the page with a full refresh (Ctrl+F5), and check the Network tab again: the .js and .css requests should now start with /user-USERNAME/myapp/.

Applications that Self-Hosted Apps on PMSS marks "Path stripping" are meant to run behind map-urlpath unchanged. An application whose documentation says it must run at the root of its own hostname does not fit the /user-USERNAME/myapp/ pattern; that page lists those too.

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