← Back to DevBytes

Lighttpd Web Server: Complete Setup and Configuration Guide

What is Lighttpd?

Lighttpd (pronounced "lighty") is an open-source, high-performance web server designed with speed, security, and low resource consumption in mind. Originally created by Jan Kneschke in 2003 as a proof-of-concept to handle the C10K problem—serving 10,000 concurrent connections on a single server—Lighttpd has evolved into a mature, production-ready HTTP server used by some of the most traffic-intensive websites in the world.

At its core, Lighttpd employs an event-driven, single-threaded architecture powered by asynchronous I/O multiplexing (using mechanisms like epoll, kqueue, or poll). This design allows it to handle thousands of simultaneous connections without spawning a separate thread or process for each request, dramatically reducing memory footprint and CPU context-switching overhead compared to traditional process-based servers like Apache's prefork model.

Lighttpd supports HTTP/1.1, HTTP/2 (via the mod_h2 module), WebSockets, FastCGI, SCGI, reverse proxying, URL rewriting, virtual hosting, SSL/TLS with SNI, and a rich module ecosystem. Its configuration syntax is straightforward and logical, making it approachable for both beginners and seasoned system administrators.

Why Lighttpd Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

In an era where infrastructure efficiency directly translates to cost savings and environmental impact, Lighttpd occupies a crucial niche. Here are the key reasons developers and operations teams choose it:

Exceptional Performance Under Load

Lighttpd's event-driven model shines when serving static assets, proxying requests, or handling long-lived connections like Server-Sent Events or WebSockets. It consistently outperforms many alternatives in benchmarks involving high concurrency, delivering lower latency and higher throughput while using a fraction of the system resources.

Minimal Memory Footprint

A typical Lighttpd process consumes only a few megabytes of RAM, even under moderate load. This makes it ideal for embedded systems, virtual private servers with limited resources, containerized environments, and edge computing deployments where every megabyte counts.

Predictable Resource Usage

Unlike process-forking servers where memory usage grows linearly with concurrent connections, Lighttpd's memory consumption remains relatively flat. This predictability simplifies capacity planning and prevents out-of-memory scenarios during traffic spikes.

Security by Design

Lighttpd maintains a strong security track record. Its codebase is compact and auditable, and it ships with sensible defaults—directory listing is disabled, request filtering is built-in, and modules can be precisely controlled. The project responds quickly to vulnerability reports and maintains a clear security advisory process.

Ideal for Microservices and CDN Edge Nodes

When deployed as a reverse proxy or static asset server at the edge, Lighttpd excels. Its ability to handle massive concurrent connections with minimal resources makes it perfect for content delivery network edge nodes, API gateways, and microservice architectures where lightweight, fast proxy layers are essential.

Installation

Lighttpd is available in the package repositories of virtually all major Linux distributions. Below are installation instructions for the most common environments.

Debian / Ubuntu

sudo apt update
sudo apt install lighttpd

After installation, the service starts automatically. Verify its status:

sudo systemctl status lighttpd

RHEL / CentOS / Fedora / AlmaLinux

On RHEL-based systems, Lighttpd is typically available via EPEL (Extra Packages for Enterprise Linux). Enable EPEL first, then install:

sudo dnf install epel-release
sudo dnf install lighttpd
sudo systemctl enable lighttpd
sudo systemctl start lighttpd

Alpine Linux

sudo apk add lighttpd
sudo rc-service lighttpd start

Arch Linux

sudo pacman -S lighttpd
sudo systemctl enable lighttpd
sudo systemctl start lighttpd

Building from Source

For environments requiring specific compile-time options or the latest features, building from source is straightforward. First, install build dependencies:

# Debian/Ubuntu
sudo apt install build-essential libpcre3-dev zlib1g-dev libssl-dev

# RHEL/Fedora
sudo dnf install gcc make pcre-devel zlib-devel openssl-devel

Download the source tarball from lighttpd.net and compile:

wget https://download.lighttpd.net/lighttpd/releases-1.4.x/lighttpd-1.4.76.tar.xz
tar xf lighttpd-1.4.76.tar.xz
cd lighttpd-1.4.76
./configure --with-openssl --with-pcre --with-zlib
make
sudo make install

The default installation prefix is /usr/local. The main configuration file will be located at /usr/local/etc/lighttpd/lighttpd.conf.

Core Configuration Structure

Lighttpd's configuration lives in a single primary file—typically /etc/lighttpd/lighttpd.conf—with optional includes from a conf.d or conf-enabled directory. The syntax is clean and procedural: settings are applied in the order they appear, and later directives can override earlier ones.

Here is the essential anatomy of a minimal working configuration:

# /etc/lighttpd/lighttpd.conf

server.modules = (
  "mod_indexfile",
  "mod_access",
  "mod_accesslog",
  "mod_staticfile"
)

server.document-root = "/var/www/html"
server.port          = 80
server.username      = "www-data"
server.groupname     = "www-data"

index-file.names     = ( "index.html", "index.htm" )

mimetype.assign      = (
  ".html" => "text/html",
  ".htm"  => "text/html",
  ".txt"  => "text/plain",
  ".css"  => "text/css",
  ".js"   => "application/javascript",
  ".png"  => "image/png",
  ".jpg"  => "image/jpeg",
  ".jpeg" => "image/jpeg",
  ".gif"  => "image/gif",
  ".svg"  => "image/svg+xml",
  ".ico"  => "image/x-icon"
)

server.errorlog      = "/var/log/lighttpd/error.log"
accesslog.filename   = "/var/log/lighttpd/access.log"

Let's break down each directive:

Working with Modules

Lighttpd's functionality is extended through modules. The base server handles only the HTTP protocol engine; everything else—logging, access control, compression, FastCGI, proxying—comes from modules. Loading a module is as simple as adding its name to server.modules.

Here are the most commonly used modules and their purposes:

To enable compression, for example, you add "mod_compress" to server.modules and configure it:

server.modules += ( "mod_compress" )

compress.cache-dir   = "/var/cache/lighttpd/compress"
compress.filetype    = ( "text/html", "text/css", "text/javascript", "application/javascript", "application/json", "text/xml", "image/svg+xml" )
compress.allowed-encodings = ( "gzip", "deflate", "br" )

Note the += operator, which appends to the existing module list rather than replacing it. This is useful in included configuration snippets.

Virtual Hosting

Lighttpd supports name-based virtual hosting (using the HTTP Host header) via the mod_simple_vhost module, or you can define virtual hosts explicitly with conditional blocks. The explicit approach offers finer control.

Explicit Virtual Hosts with Conditionals

Use the $HTTP["host"] conditional to match specific hostnames:

server.modules += ( "mod_accesslog" )

# Default (catch-all) host
server.document-root = "/var/www/default"

$HTTP["host"] == "example.com" {
  server.document-root = "/var/www/example.com"
  accesslog.filename    = "/var/log/lighttpd/example.com.access.log"
}

$HTTP["host"] == "api.example.com" {
  server.document-root = "/var/www/api"
  # Different configuration for the API subdomain
}

$HTTP["host"] =~ "^(.+\.)?blog\.example\.com$" {
  server.document-root = "/var/www/blog"
  # Regex match catches blog.example.com and sub.blog.example.com
}

Conditionals can be nested, allowing you to match on host plus other criteria:

$HTTP["host"] == "secure.example.com" {
  $SERVER["socket"] == ":443" {
    ssl.pemfile = "/etc/lighttpd/certs/secure.example.com.pem"
  }
}

Simple Virtual Hosting

For scenarios where you want virtual hosts to map directly to directories named after the hostname, mod_simple_vhost automates this:

server.modules += ( "mod_simple_vhost" )

simple-vhost.server-root   = "/var/www/vhosts"
simple-vhost.document-root = "/htdocs"
simple-vhost.default-host  = "default.example.com"

With this setup, a request for example.com serves files from /var/www/vhosts/example.com/htdocs/. The default-host handles requests that don't match any existing directory.

SSL/TLS Configuration

Securing your server with HTTPS is essential. Lighttpd uses mod_openssl (built on OpenSSL) for TLS termination. The configuration requires a certificate and private key, typically combined in a single PEM file.

Generating Certificates with Let's Encrypt

Use Certbot or another ACME client to obtain certificates:

sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com

Certbot places the certificate at /etc/letsencrypt/live/example.com/fullchain.pem and the private key at /etc/letsencrypt/live/example.com/privkey.pem. Lighttpd expects both in one file, so concatenate them:

sudo cat /etc/letsencrypt/live/example.com/privkey.pem \
         /etc/letsencrypt/live/example.com/fullchain.pem \
         > /etc/lighttpd/certs/example.com.pem
sudo chmod 600 /etc/lighttpd/certs/example.com.pem

Enabling TLS

Add mod_openssl to your modules and configure the listening socket:

server.modules += ( "mod_openssl" )

$SERVER["socket"] == ":443" {
  ssl.engine  = "enable"
  ssl.pemfile = "/etc/lighttpd/certs/example.com.pem"
  ssl.use-sslv2   = "disable"
  ssl.use-sslv3   = "disable"
  ssl.cipher-list = "ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM"
  ssl.honor-cipher-order = "enable"
}

# Optional: redirect HTTP to HTTPS
$HTTP["scheme"] == "http" {
  $HTTP["host"] =~ "^(.*)$" {
    url.redirect = (".*" => "https://%1$0")
  }
}

Server Name Indication (SNI)

To serve multiple domains with different certificates on the same IP and port, use SNI matching:

$SERVER["socket"] == ":443" {
  ssl.engine = "enable"

  $HTTP["host"] == "example.com" {
    ssl.pemfile = "/etc/lighttpd/certs/example.com.pem"
  }
  $HTTP["host"] == "another.org" {
    ssl.pemfile = "/etc/lighttpd/certs/another.org.pem"
  }
  # Fallback certificate for unmatched hosts
  ssl.pemfile = "/etc/lighttpd/certs/default.pem"
}

Enabling HTTP/2

Once TLS is configured, HTTP/2 can be enabled with mod_h2:

server.modules += ( "mod_h2" )

$SERVER["socket"] == ":443" {
  ssl.engine = "enable"
  ssl.pemfile = "/etc/lighttpd/certs/example.com.pem"
  h2.protocol = "enable"
}

PHP Integration via FastCGI

Lighttpd does not embed a PHP interpreter. Instead, it communicates with PHP-FPM (FastCGI Process Manager) via the FastCGI protocol. This separation of concerns improves security and allows independent scaling of the PHP backend.

Installing and Configuring PHP-FPM

# Debian/Ubuntu
sudo apt install php-fpm

# RHEL/Fedora
sudo dnf install php-fpm

Ensure PHP-FPM listens on a Unix socket or TCP socket. The default pool configuration typically uses a Unix socket at /run/php/php-fpm.sock or /var/run/php/php8.3-fpm.sock. Check your distribution's specifics:

sudo grep 'listen' /etc/php/*/fpm/pool.d/www.conf

Wiring Lighttpd to PHP-FPM

Add mod_fastcgi to your modules and configure the handler:

server.modules += ( "mod_fastcgi" )

fastcgi.server = (
  ".php" => ((
    "socket"      => "/run/php/php-fpm.sock",
    "bin-path"    => "/usr/bin/php-cgi",
    "max-procs"   => 4,
    "broken-scriptfilename" => "enable",
    "allow-x-sendfile" => "enable"
  ))
)

For TCP-based PHP-FPM (e.g., when using containers or remote backends):

fastcgi.server = (
  ".php" => ((
    "host"        => "127.0.0.1",
    "port"        => 9000,
    "broken-scriptfilename" => "enable"
  ))
)

Path Info Handling

Some PHP applications (like WordPress in certain configurations) rely on PATH_INFO. Configure it explicitly:

fastcgi.server = (
  ".php" => ((
    "socket"      => "/run/php/php-fpm.sock",
    "bin-path"    => "/usr/bin/php-cgi",
    "max-procs"   => 4,
    "broken-scriptfilename" => "enable",
    "fix-pathinfo" => 1
  ))
)

Complete PHP Hosting Configuration Example

server.modules = (
  "mod_indexfile",
  "mod_access",
  "mod_accesslog",
  "mod_staticfile",
  "mod_fastcgi",
  "mod_setenv"
)

server.document-root = "/var/www/html"
server.port          = 80
server.username      = "www-data"
server.groupname     = "www-data"

index-file.names     = ( "index.php", "index.html", "index.htm" )

mimetype.assign      = (
  ".html" => "text/html",
  ".php"  => "application/x-httpd-php",
  ".css"  => "text/css",
  ".js"   => "application/javascript"
)

fastcgi.server = (
  ".php" => ((
    "socket"      => "/run/php/php-fpm.sock",
    "bin-path"    => "/usr/bin/php-cgi",
    "max-procs"   => 4,
    "broken-scriptfilename" => "enable"
  ))
)

# Security: deny access to hidden files and sensitive paths
$HTTP["url"] =~ "/\." {
  url.access-deny = ("")
}

static-file.exclude-extensions = ( ".php", ".inc", ".sql", ".bak" )

Reverse Proxy Configuration

Lighttpd excels as a reverse proxy, forwarding client requests to backend application servers. The mod_proxy module handles HTTP proxying, while mod_fastcgi handles FastCGI backends.

Basic HTTP Reverse Proxy

server.modules += ( "mod_proxy" )

$HTTP["host"] == "app.example.com" {
  proxy.server = ( "" => ((
    "host" => "127.0.0.1",
    "port" => 3000
  )))
}

This forwards all requests matching app.example.com to a Node.js or Python application listening on port 3000.

Advanced Proxy with Headers

When proxying, you should forward relevant client headers so the backend knows the original requester:

$HTTP["host"] == "app.example.com" {
  proxy.server = ( "" => ((
    "host" => "127.0.0.1",
    "port" => 3000
  )))

  # Preserve original request information
  setenv.add-request-header = (
    "X-Forwarded-For"   => "%{REMOTE_ADDR}e",
    "X-Forwarded-Proto" => "%{SCHEME}e",
    "X-Forwarded-Host"  => "%{HTTP_HOST}e"
  )
}

Proxy with Load Balancing

Lighttpd can distribute requests across multiple backends using weighted or round-robin balancing:

proxy.balance = "round-robin"

$HTTP["host"] == "api.example.com" {
  proxy.server = ( "" => (
    ( "host" => "10.0.1.10", "port" => 8080 ),
    ( "host" => "10.0.1.11", "port" => 8080 ),
    ( "host" => "10.0.1.12", "port" => 8080 )
  ))

  proxy.balance = "fair"  # or "round-robin", "least-connections"
}

WebSocket Proxy

For WebSocket connections, use mod_wstunnel alongside mod_proxy:

server.modules += ( "mod_wstunnel", "mod_proxy" )

$HTTP["url"] =~ "^/ws/" {
  wstunnel.server = ( "" => ((
    "host" => "127.0.0.1",
    "port" => 8080
  )))
  wstunnel.frame-type = "text"
}

URL Rewriting and Redirects

Lighttpd provides two modules for URL manipulation: mod_redirect for external redirects (HTTP 301/302) and mod_rewrite for internal rewrites (the URL changes internally but the client sees the original).

Redirects

server.modules += ( "mod_redirect" )

# Redirect old page to new location
$HTTP["url"] == "/old-page.html" {
  url.redirect = (".*" => "/new-page.html")
}

# Canonicalize www to non-www (or vice versa)
$HTTP["host"] == "www.example.com" {
  url.redirect = (".*" => "https://example.com$0")
}

# Force HTTPS
$HTTP["scheme"] == "http" {
  $HTTP["host"] =~ "example.com" {
    url.redirect = (".*" => "https://example.com$0")
  }
}

Internal Rewrites

Rewrites are essential for clean URLs in CMS platforms and web applications:

server.modules += ( "mod_rewrite" )

# WordPress-style front controller
url.rewrite-if-not-file = (
  "^/(wp-admin|wp-includes)/(.*)" => "$0",
  "^/(.*)$" => "/index.php/$1"
)

# Drupal-style clean URLs
url.rewrite-if-not-file = (
  "^/(.*)$" => "/index.php?q=$1"
)

The url.rewrite-if-not-file directive only rewrites URLs that don't correspond to existing files on disk, preventing static assets from being incorrectly routed to the front controller.

Access Control and Security

Lighttpd's mod_access provides flexible access control. You can deny or allow requests based on IP address, URL patterns, HTTP methods, and more.

IP-Based Access Control

# Deny by default, allow specific IPs for admin area
$HTTP["url"] =~ "^/admin" {
  $HTTP["remoteip"] !~ "^(10\.0\.0\.|192\.168\.1\.|127\.0\.0\.1)" {
    url.access-deny = ("")
  }
}

# Allow only specific IP ranges
$HTTP["remoteip"] =~ "^(203\.0\.113\.|198\.51\.100\.)" {
  # Trusted IPs get full access
} else {
  $HTTP["url"] =~ "^/internal" {
    url.access-deny = ("")
  }
}

Blocking Sensitive Files

# Deny access to hidden files and backup files
$HTTP["url"] =~ "/\.|\~$|\.bak$|\.sql$|\.inc$|\.env$" {
  url.access-deny = ("")
}

# Prevent access to version control directories
$HTTP["url"] =~ "^/(\.git|\.svn|\.hg)/" {
  url.access-deny = ("")
}

HTTP Method Restrictions

# Only allow GET and POST on the main site
$HTTP["host"] == "example.com" {
  $HTTP["request-method"] !~ "^(GET|POST|HEAD)$" {
    url.access-deny = ("")
  }
}

Rate Limiting Basics

While Lighttpd doesn't have built-in rate limiting, you can achieve basic protection using mod_access combined with connection limits:

server.max-connections = 500
server.max-connections-per-ip = 10

For more sophisticated rate limiting, consider placing Lighttpd behind a dedicated load balancer or using firewall rules with iptables or nftables.

Logging and Monitoring

Proper logging is crucial for debugging, security auditing, and performance analysis. Lighttpd provides both access logging and error logging with flexible formatting options.

Access Log Customization

server.modules += ( "mod_accesslog" )

accesslog.format = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O"
accesslog.filename = "/var/log/lighttpd/access.log"

The format string uses Apache-compatible placeholders. The %I and %O tokens log incoming and outgoing byte counts, which are invaluable for bandwidth monitoring.

Conditional Logging

Log different hosts to separate files:

$HTTP["host"] == "example.com" {
  accesslog.filename = "/var/log/lighttpd/example.access.log"
}

$HTTP["host"] == "api.example.com" {
  accesslog.filename = "/var/log/lighttpd/api.access.log"
}

Error Log Verbosity

server.errorlog = "/var/log/lighttpd/error.log"
debug.log-request-header = "disable"
debug.log-response-header = "disable"
debug.log-file-not-found = "enable"

Enable debug.log-file-not-found during development to track missing assets; disable it in production to reduce log noise.

Performance Tuning

Lighttpd is fast out of the box, but these tuning parameters can help you extract maximum performance for your specific workload.

Connection and Threading Tuning

server.max-connections = 2000
server.max-connections-per-ip = 50
server.max-fds = 4096
server.listen-backlog = 1024

Static File Caching

server.modules += ( "mod_expire" )

$HTTP["url"] =~ "\.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$" {
  expire.url = ( "" => "access 30 days" )
}

# Cache immutable assets for a year
$HTTP["url"] =~ "\.(webp|avif|woff2)$" {
  expire.url = ( "" => "access 365 days" )
}

Compression Tuning

compress.cache-dir   = "/var/cache/lighttpd/compress"
compress.filetype    = ( "text/html", "text/css", "text/javascript", "application/javascript", "application/json", "text/xml", "image/svg+xml" )
compress.allowed-encodings = ( "gzip", "br" )

Use Brotli (br) alongside gzip for modern browsers; Lighttpd selects the best encoding based on the client's Accept-Encoding header.

Kernel and System Tuning

Beyond Lighttpd's configuration, tune the operating system for high-concurrency serving:

# /etc/sysctl.conf additions
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 100000

Apply with sudo sysctl -p. Also ensure the www-data user has adequate file descriptor limits in /etc/security/limits.conf.

Containerization and Deployment

Lighttpd is exceptionally well-suited for containerized environments. Its small footprint, fast startup, and single-binary design align perfectly with Docker and Kubernetes patterns.

Minimal Dockerfile

FROM alpine:3.20

RUN apk add --no-cache lighttpd lighttpd-mod_openssl lighttpd-mod_fastcgi

COPY lighttpd.conf /etc/lighttpd/lighttpd.conf
COPY html/ /var/www/html/

EXPOSE 80 443

CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"]

The -D flag tells Lighttpd to run in the foreground (don't daemonize), which is required for container process supervision.

Docker Compose Example with PHP-FPM

version: "3.9"
services:
  web:
    build: .
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./html:/var/www/html
      - ./certs:/etc/lighttpd/certs
    depends_on:
      - php

  php:
    image: php:8.3-fpm-alpine
    volumes:
      - ./html:/var/www/html
    expose:
      - "9000"

In the Lighttpd configuration, point FastCGI to the PHP service over TCP:

fastcgi.server = (
  ".php" => ((
    "host" => "php",
    "port" => 9000,
    "broken-scriptfilename" => "enable"
  ))
)

Best Practices Summary

Over years of production deployments, certain patterns have emerged as consistently reliable. Adopt these practices to build a robust, maintainable Lighttpd setup.

1. Explicit Module Loading

Always define server.modules explicitly rather than relying on defaults. This ensures you know exactly what's running and minimizes the attack surface.

server.modules = (
  "mod_indexfile",
  "mod_access",
  "mod_accesslog",
  "mod_staticfile",
  "mod_fastcgi",
  "mod_openssl",
  "mod_h2"
)

2. Split Configuration into Included Files

Use the include directive to organize configuration logically:

include "/etc/lighttpd/conf.d/mime.conf"
include "/etc/lighttpd/conf.d/ssl.conf"
include "/etc/lighttpd/conf.d/vhosts/*.conf"

This makes maintenance easier and allows automated tooling (like Certbot) to drop in configuration snippets safely.

3. Test Configuration Before Reloading

Always validate your configuration syntax before applying changes:

lighttpd -t -f /etc/lighttpd/lighttpd.conf

Only if the test passes should you reload:

sudo systemctl reload lighttpd

4. Run as an Unprivileged User

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles