FlashLin

Documentation

Domain configuration and HTTPS

A full walkthrough of virtual hosts, routing, and automatic HTTPS via Let's Encrypt — no control panel required.

FlashLin has no admin dashboard, no separate certbot process, and no central config that grows unbounded as you add sites. Every domain is one plain TOML file, and certificates renew themselves. This page walks through the whole thing: adding a domain, routing it, and turning on Let's Encrypt.

How virtual hosts work

Point domains_dir at a folder in your main server.toml:

[server]
host        = "0.0.0.0"
port        = 80
root        = "/var/www/public"
domains_dir = "/etc/flashlin/domains"

Every *.toml file FlashLin finds there becomes one virtual host. There's no registration step and no reload needed to pick up the directory itself — add, edit, or remove a file and flashlin --watch picks up the change within two seconds (see Hot reload below). A request's Host header is matched against each file's domain.name and domain.aliases; anything that matches no virtual host falls through to the global [[routes]] in server.toml.

Adding a domain

Create one file per site:

# /etc/flashlin/domains/example.com.toml

[domain]
name        = "example.com"
aliases     = ["www.example.com"]
root        = "/var/www/example.com/public"
index       = ["index.html", "index.php"]
server_name = "Example Site"   # shown in error pages; defaults to `name`

[[routes]]
path  = "/"
root  = "/var/www/example.com/public"
index = "index.html"

That's a complete, working virtual host. aliases covers alternate hostnames that should serve the same site (www. variants, old domains you're redirecting traffic from) without duplicating the whole block.

Multiple domains

Nothing links these files together — each is independent, so a second site is just a second file:

# /etc/flashlin/domains/blog.example.com.toml

[domain]
name = "blog.example.com"
root = "/var/www/blog/public"

[[routes]]
path = "/"
root = "/var/www/blog/public"

Reverse-proxying a domain to an app backend

A domain's routes work exactly like the global ones — including proxy_pass, so a domain can front an application server instead of static files:

[domain]
name = "app.example.com"
root = "/var/www/app.example.com/public"   # still needed for e.g. error pages

[[routes]]
path       = "/"
proxy_pass = "http://127.0.0.1:3000"

[[routes]]
path = "/assets"
root = "/var/www/app.example.com/public/assets"

See QUERY and WebSockets for proxying WebSocket upgrades and the HTTP QUERY method to that backend.

Per-domain overrides

PHP, security limits, and compression all inherit from the global server.toml unless a domain overrides them:

[php]
enabled         = true
default_handler = "php82"

[[php.handlers]]
name          = "php82"
mode          = "fpm"
socket        = "/run/php/php8.2-fpm.sock"
document_root = "/var/www/example.com/public"
timeout       = "30s"

[security]
max_body_size = "5MB"
rate_limit    = { limit = "200/min", burst = 20 }

[compression]
gzip   = true
brotli = true

Automatic HTTPS with Let's Encrypt

This needs a binary built with the acme feature — cargo build --release --locked --features acme (already included in every production package). Enable it in server.toml, not per-domain:

[tls]
enabled                 = true
http_port               = 80
redirect_http_to_https  = true

[tls.acme]
domains      = ["example.com", "www.example.com"]
email        = "admin@example.com"
cache_dir    = "/etc/flashlin/acme"
production   = false    # staging first — see below
accept_terms = false    # read the CA agreement before setting true

One [tls.acme] block issues one certificate covering every domain in its domains list (up to 100 names) — it's not automatic per-virtual-host certificate management. If you're serving several unrelated domains that each need their own certificate, either list them all here (they'll share one certificate) or use manual certificates per domain instead (below).

Before this will work:

  • Every domain in domains must have a public DNS record pointing at this server, including an AAAA record if you have IPv6.
  • Public TCP port 443 must reach FlashLin's listener for TLS-ALPN-01 validation. A CDN or TLS-terminating proxy in front of FlashLin will intercept the handshake and break validation.
  • Port 80 is only used for the HTTP→HTTPS redirect, not the ACME challenge itself.
  • Wildcard certificates and DNS-01 validation aren't supported — every name needs to resolve and be reachable directly.

Go through staging first. production = false gets you a Let's-Encrypt-issued certificate that isn't browser-trusted, specifically so you can confirm DNS, ports, and your domain list are all correct without touching Let's Encrypt's real-certificate rate limits. Once a staging run succeeds — check the logs for certificate deployment — flip production = true and restart.

$ flashlin --watch /etc/flashlin/server.toml
=> example.com: certificate issued (Let's Encrypt, staging)
=> FlashLin listening on 0.0.0.0:443 (TLS)

cache_dir holds the ACME account key and issued certificates — treat it like any other private key material. It must live outside every served document root (FlashLin's config validation rejects a cache directory inside a public root), persist across restarts, and stay unreadable to other users: Unix creates it with mode 0700 and refuses to reuse an existing directory that's group- or world-accessible.

Manual certificates per domain

If you already have certificates from another CA, or need independent certificates for domains that shouldn't share one ACME registration, set TLS directly on the virtual host instead:

[domain]
name = "example.com"
root = "/var/www/example.com/public"

[tls]
enabled = true
cert    = "/etc/flashlinssl/certs/example.com.crt"
key     = "/etc/flashlinssl/certs/example.com.key"

[[routes]]
path = "/"
root = "/var/www/example.com/public"

Manual certificates and ACME are mutually exclusive per configuration — choose one approach for a given domain, not both.

Validating before you deploy

$ flashlin --check /etc/flashlin/server.toml
Configuration OK: /etc/flashlin/server.toml

--check parses and validates everything — including every file in domains_dir — without binding a port or contacting Let's Encrypt. It catches duplicate hostnames, missing domain directories, and malformed certificate/key pairs before they'd otherwise fail silently on reload.

Applying changes without downtime

$ flashlin --watch /etc/flashlin/server.toml

--watch polls for changes every two seconds (Windows and Unix both) and hot-swaps the entire configuration — main server.toml, every file in domains_dir (additions and removals included), routes, security rules, and manual certificates — without dropping a connection. In-flight requests finish under the settings they started with; the next new request picks up whatever generation is current.

An invalid change (bad TOML, a duplicate host, a missing cert file) is rejected and logged — the previous working configuration keeps running untouched. Listener address/port, worker count, and ACME account settings are the exceptions: those require a full restart, and a reload attempt that changes them is rejected outright rather than partially applied.

For a container, set FLASHLIN_WATCH=1 on the entrypoint instead of passing --watch directly.

Next steps