Skip to content

Architecture

All containers join the rootless Podman network lerd. Communication between Nginx and PHP-FPM uses container names as hostnames.

Request flow

graph TD
    Browser["Browser"] -->|port 80/443| Nginx["lerd-nginx<br/>(nginx:alpine)"]
    Nginx -->|fastcgi_pass :9000| FPM["lerd-php84-fpm<br/>(locally built image)"]
    FPM -->|reads| Files["~/Lerd/my-app<br/>(bind-mounted read-only)"]

    DNS["*.test DNS"] -->|resolves to| Loopback["127.0.0.1"]
    Loopback --> Nginx

    NM["NetworkManager"] -->|forwards .test queries| DNSMASQ["lerd-dns<br/>(dnsmasq, port 5300)"]
    DNSMASQ -->|returns| Loopback

Components

Component Technology
CLI Go + Cobra, single static binary
Web server Podman Quadlet — nginx:alpine
PHP-FPM Podman Quadlet per version — locally built image with all Laravel extensions
PHP CLI php binary inside the FPM container (podman exec)
Composer composer.phar via bundled PHP CLI
Node fnm binary, version per project
Services Podman Quadlet containers
DNS dnsmasq container + NetworkManager integration
TLS mkcert — locally trusted CA

Key design decisions

Rootless Podman — all containers run without root privileges. The only operations requiring sudo are DNS setup (writes to /etc/NetworkManager/) and the initial net.ipv4.ip_unprivileged_port_start=80 sysctl.

Podman Quadlets — containers are defined as systemd unit files (.container files) managed by the Quadlet generator. This means systemctl --user start lerd-nginx works like any other systemd service, and containers restart on failure and at login.

Shared nginx — a single nginx container serves all sites via virtual hosts. nginx uses a Podman-network-aware resolver to route fastcgi_pass to the correct PHP-FPM container by hostname.

Per-version PHP-FPM — each PHP version gets its own container built from a local Containerfile. The image includes all extensions needed for Laravel out of the box: pdo_mysql, pdo_pgsql, bcmath, mbstring, xml, zip, gd, intl, opcache, pcntl, exif, sockets, redis, imagick.