Configuración
Referencia de las secciones del IR nativo (TOML), campo a campo, con un ejemplo mínimo por sección. El esquema lo define el propio producto: cualquier campo no listado aquí se rechaza con unknown field. Valida siempre con exyonq validate -c <fichero>.
config_version
Obligatorio. Versiones admitidas: 1 y 2. La 2 es la actual y la única que permite include, una lista de ficheros que se fusionan con el documento principal.
| Campo | Tipo | Defecto | Notas |
|---|
config_version | entero | obligatorio | 1 o 2; otro valor da unsupported config_version |
include | lista de rutas | vacía | Solo con config_version = 2 |
config_version = 2
include = ["/etc/exyonq/tls.toml", "/etc/exyonq/sites.d/blog.toml"]
[logging]
Niveles y destinos. level acepta la sintaxis de EnvFilter: info, debug, o por objetivo (exyonq=debug,warn).
| Campo | Tipo | Defecto | Notas |
|---|
level | string | "info" | No vacío |
format | string | "text" | text o json |
queue_capacity | entero | 8192 | Cola de sinks asíncronos, 1..=1048576 |
[logging.console] | tabla | enabled = true | stream: stdout, stderr o split |
[logging.file] | tabla | enabled = false | path obligatorio si se habilita; rotation.enabled, max_bytes (100 MiB), keep (7) |
[logging.syslog] | tabla | enabled = false | transport: unix, udp o tcp; address obligatorio |
[logging.journald] | tabla | enabled = false | Envío estructurado nativo a journald |
[logging.otel] | tabla | enabled = false | endpoint http(s) obligatorio; metrics_export está prohibido |
[logging.access] | tabla | enabled = true | Una línea por petición |
[logging.audit] | tabla | enabled = true | Eventos de auditoría |
[logging]
level = "info"
format = "text"
[logging.console]
enabled = true
stream = "stdout"
[logging.access]
enabled = true
[[server]]
Un bloque por conjunto de nombres que comparten socket. listen es obligatorio; el resto, opcional.
| Campo | Tipo | Defecto | Notas |
|---|
listen | string | obligatorio | IP:puerto o :puerto; cada dirección se abre |
server_name | string o lista | vacío | Nombre DNS o *.sufijo; base del SNI y de strict_sni |
routes | lista de nombres | vacía | Rutas declaradas para el bloque; un nombre inexistente es error |
error_pages | tabla estado → ruta | vacía | Claves string ("404"), valor ruta absoluta; el estado se conserva |
proxy_protocol | tabla | ausente | enabled y trusted (CIDR); sin trusted se acepta de cualquier par |
http3_listen | string | ausente | Solo en el primer [[server]] |
[server.tls] | tabla | ausente | Ver más abajo |
[[server]]
listen = "0.0.0.0:8080"
server_name = ["exyonq.dev", "www.exyonq.dev"]
routes = ["site"]
proxy_protocol = { enabled = true, trusted = ["10.0.0.0/8"] }
[server.error_pages]
"404" = "/404.html"
"503" = "/maintenance.html"
[server.tls]
cert y key son obligatorios si la tabla existe. Los campos criptográficos solo se honran en el primer bloque de un listen compartido; declararlos en otro bloque es error, no un ignorado silencioso.
| Campo | Tipo | Defecto | Notas |
|---|
cert | ruta | obligatorio | PEM con la cadena de certificados |
key | ruta | obligatorio | PEM con la clave privada |
strict_sni | bool | false | Rechaza handshakes cuyo SNI no case con ningún server_name; exige al menos uno |
protocols | lista | vacía (rustls: 1.2 y 1.3) | TLSv1.2, TLSv1.3; rustls no soporta versiones anteriores |
ciphers | lista | vacía (rustls) | Nombres IANA u OpenSSL, o lista separada por :; solo el juego AEAD de rustls |
curves | lista | vacía (rustls) | X25519, secp256r1/prime256v1/P-256, secp384r1/P-384 |
ocsp_stapling | ruta | ausente | Respuesta OCSP en DER; ExyonQ no la descarga del respondedor |
must_staple | bool | false | Exige ocsp_stapling; sin él, configuración rechazada |
[server.tls.acme] | tabla | ausente | Ver la sección siguiente |
[server.tls]
cert = "/etc/exyonq/tls/exyonq.dev.pem"
key = "/etc/exyonq/tls/exyonq.dev.key"
strict_sni = true
protocols = ["TLSv1.2", "TLSv1.3"]
ciphers = ["TLS13_AES_256_GCM_SHA384", "TLS13_CHACHA20_POLY1305_SHA256"]
curves = ["X25519", "secp256r1"]
ocsp_stapling = "/etc/exyonq/tls/exyonq.dev.ocsp.der"
must_staple = false
[server.tls.acme]
Emisión y renovación automáticas. Solo se honra en el primer [[server]].
| Campo | Tipo | Defecto | Notas |
|---|
enabled | bool | true | |
email | string | obligatorio | Contacto de la cuenta ACME |
domains | lista | obligatorio | Nombres a emitir; el comodín exige dns-01 |
staging | bool | false | Usa el entorno de pruebas de la CA |
challenge | string | "http-01" | http-01 o dns-01 |
dns_hook | string | ausente | Comando para publicar el TXT; variables EXYONQ_ACME_* |
dns_propagation_secs | entero | 15 | Espera tras publicar el TXT; solo con dns-01 |
account_path | ruta | <cert>.acme-account.json | Credenciales de cuenta, modo 0600 |
[server.tls.acme]
enabled = true
email = "security@exyonq.dev"
domains = ["exyonq.dev", "www.exyonq.dev"]
challenge = "http-01"
[[route]]
| Campo | Tipo | Defecto | Notas |
|---|
name | string | obligatorio | Único en toda la configuración |
match | tabla | obligatorio | Ver tabla siguiente |
root | ruta | — | Raíz estática; actúa como alias del prefijo |
index | string | — | Fichero por defecto del directorio |
upstream | string | — | Nombre de [[upstream]] |
fastcgi | string | — | Nombre de [[fcgi_pool]] |
redirect | tabla | — | status (302 por defecto) y location |
rewrite | string o tabla | — | Ruta literal, o pattern/replacement/flag/when |
cache | string | — | Nombre de [[cache_policy]] |
cache_tags | lista | vacía | Etiquetas para purgar por tag |
cache_headers | tabla | — | by_mime: MIME → max_age e immutable |
response_headers | lista | vacía | name, value, always; una entrada por nombre |
proxy_set_headers | lista | vacía | name, value; plantillas con variables |
location_rewrite | lista | vacía | from, to: reescribe Location del backend |
fallback_rewrite | string | — | Tras un fallo estático, reintenta una vez esa ruta |
try_uri / try_uri_dir | bool | — | Sondean URI como fichero / directorio antes del fallback |
htaccess | string | "off" | overlay exige raíz documental y no admite redirect ni proxy |
Campos de match:
| Campo | Tipo | Defecto | Notas |
|---|
path | string | "" | Prefijo; XOR con suffix y regex |
suffix | string | ausente | Sufijo literal |
regex | string | ausente | Regex de Rust, máximo 256 bytes |
host | string | ausente | Host exacto o *.sufijo |
header | string | ausente | Filtro AND por cabecera |
header_value | string | ausente | Valor exacto; sin él, la cabecera debe existir y no estar vacía |
[[route]]
name = "assets"
match = { path = "/assets", host = "exyonq.dev" }
root = "/var/www/exyonq/assets"
response_headers = [{ name = "Strict-Transport-Security", value = "max-age=31536000; includeSubDomains" }]
[route.cache_headers.by_mime]
"image/avif" = { max_age = 31536000, immutable = true }
redirect emite location tal cual, sin plantillas: una ruta por host si quieres conservar el destino. rewrite admite flag = "last" | "break" | "redirect" | "permanent" y condiciones when (path, host, query, header, header_value, todas AND). El reemplazo admite $1–$9, $uri y $host.
[[upstream]]
| Campo | Tipo | Defecto | Notas |
|---|
name | string | obligatorio | Único; lo referencian las rutas |
target | string | — | URI http://host:puerto; XOR con endpoints |
endpoints | lista | — | address, port, weight, priority, max_fails, fail_timeout_ms |
timeout_ms | entero | 30000 | Presupuesto total; máximo 86 400 000 |
max_connect_retries | entero | 1 | Solo 0 o 1 |
selection_policy | string | weighted_round_robin | least_conn, power_of_two, consistent_hash, cookie_affinity, source_affinity, header_affinity |
failover_policy | string | priority_bands | o none |
sticky_cookie | string | EXYONQ_ROUTE con afinidad por cookie | Nombre de cookie, token HTTP válido |
sticky_header | string | — | Obligatorio con header_affinity |
dns_refresh_ms | entero | 0 (off) | Re-resolución periódica; máximo 24 h |
[upstream.health_check] | tabla | enabled = false | Ver más abajo |
health_check | Tipo | Defecto | Notas |
|---|
enabled | bool | false | |
interval_ms | entero | 5000 | 100..=3600000 |
timeout_ms | entero | 1000 | 1..=60000 y ≤ interval_ms |
path | string | "/health" | Ruta sondeada |
healthy_threshold / unhealthy_threshold | entero | 2 / 3 | 1..=32 |
expect_status | lista | vacía (cualquier 2xx) | Códigos 200..=599 |
expect_body | string | ausente | Subcadena en los primeros 64 KiB |
expect_header | string | ausente | "Nombre" (presencia) o "Nombre: valor" |
[[upstream]]
name = "app"
selection_policy = "least_conn"
timeout_ms = 15000
endpoints = [
{ address = "10.0.1.11", port = 8080, weight = 3 },
{ address = "10.0.1.12", port = 8080, weight = 3 },
]
[upstream.health_check]
enabled = true
path = "/health"
expect_status = [200]
[[fcgi_pool]] (FastCGI / PHP-FPM)
| Campo | Tipo | Defecto | Notas |
|---|
name | string | obligatorio | Lo referencia fastcgi en la ruta |
address | string | obligatorio | Socket Unix o host:puerto |
transport | string | "unix" | unix o tcp |
document_root | ruta | ausente | Necesario para htaccess = "overlay" |
max_concurrency | entero | 16 | 1..=4096 |
max_connections | entero | = max_concurrency | 1..=max_concurrency |
idle_timeout_ms | entero | 30000 | 1..=3600000 |
total_timeout_ms | entero | 30000 | 1..=3600000 |
checkout_timeout_ms | entero | 5000 | 1..=600000 |
[[fcgi_pool]]
name = "php"
address = "/run/php/php-fpm.sock"
transport = "unix"
document_root = "/var/www/html"
max_concurrency = 16
total_timeout_ms = 120000
[modules.compression]
| Campo | Tipo | Defecto | Notas |
|---|
enabled | bool | false | |
min_bytes | entero | 256 | No comprime respuestas menores |
level | entero | 1 | 1..=9; 1 es el más rápido |
[modules.compression]
enabled = true
min_bytes = 256
level = 5
El resto de módulos de la misma tabla: [modules.metrics] (enabled, path = /metrics, health_path, scrape_bearer_token obligatorio si se escucha en una dirección no local), [modules.ratelimit] (enabled, requests_per_second = 100, burst = 200, zones, routes) y [modules.real_ip] (enabled, trusted, header = x-forwarded-for; con enabled = true exige al menos un CIDR de confianza).
Módulos y campos de 0.9.0
[modules.gunzip]: enabled (off) y buffer_bytes (32 KiB); una ruta puede forzar gunzip = true|false.[modules.auth.routes.<ruta>]: realm y user_file (htpasswd) para auth básica.[modules.ratelimit.zones.<zona>]: key (ip, host o header:<nombre>), requests_per_second y burst; [modules.ratelimit.routes] asigna ruta → zona.[modules.sub_filter.routes.<ruta>]: rules = [{ pattern, replacement }], once (true), types (["text/html"]) y last_modified (false).[modules.mirror.routes.<ruta>]: destinations (rutas internas) y request_body (true).[modules.slice.routes.<ruta>]: size en bytes (1 KiB..1 GiB).- En
[[route]]: front_controller (script de respaldo para FastCGI) y root_mode (remainder o full-uri).
[modules.gunzip]
enabled = true
[modules.auth.routes.site]
realm = "Admin"
user_file = "/etc/exyonq/admin.htpasswd"
[modules.ratelimit]
enabled = true
[modules.ratelimit.zones.site]
key = "ip"
requests_per_second = 20
burst = 40
[modules.ratelimit.routes]
site = "site"
[modules.sub_filter.routes.site]
rules = [{ pattern = "http://old.example", replacement = "https://new.example" }]
[modules.mirror.routes.site]
destinations = ["/_mirror"]
[modules.slice.routes.site]
size = 1048576
El ejemplo junta los bloques en una sola ruta para mostrarlos; en producción se reparten entre las rutas que los necesiten.
[static.preload]
Límites de la precarga del árbol estático. Un cero en cualquiera de las tres claves desactiva la precarga entera; no significa «sin límite».
| Campo | Tipo | Defecto | Notas |
|---|
max_file_bytes | entero | 33 554 432 (32 MiB) | Máximo por fichero; techo 256 MiB |
max_total_bytes | entero | 268 435 456 (256 MiB) | Techo 2 GiB |
max_entries | entero | 4096 | Techo 1 000 000 |
[static.preload]
max_file_bytes = 33554432
max_total_bytes = 268435456
max_entries = 4096
[static.encoding_cache] es una caché de compresión en disco, apagada por defecto: enabled, cache_dir (/var/cache/exyonq/static-encoding), level (6), min_bytes (300), max_bytes (10 MiB), max_entries (4096), max_total_bytes (1 GiB), gzip (true) y brotli (false).
[full_page_cache]
| Campo | Tipo | Defecto | Notas |
|---|
enabled | bool | false | |
max_entries | entero | 10000 | |
max_total_bytes | entero | 67 108 864 (64 MiB) | |
max_object_bytes | entero | 1 048 576 (1 MiB) | |
default_ttl_seconds | entero | 30 | |
max_ttl_seconds | entero | 3600 | |
namespace | entero | 4 | Debe ser distinto de cero si está habilitada |
stale_if_error_secs | entero | 0 (off) | Sirve la entrada caducada si el backend falla |
private_cookies | lista | vacía | Si la petición trae alguna, no se cachea |
[full_page_cache.key] | tabla | vacía | query, cookies, headers que entran en la clave |
[full_page_cache.bypass] | tabla | vacía | cookies, headers que fuerzan bypass |
[full_page_cache]
enabled = true
default_ttl_seconds = 300
max_ttl_seconds = 3600
namespace = 4
private_cookies = ["wordpress_logged_in", "wp-postpass"]
[full_page_cache.key]
query = ["page", "p"]
[http3]
| Campo | Tipo | Defecto | Notas |
|---|
enabled | bool | true | Por sí solo no abre socket: hace falta http3_listen |
provider | string | "s2n" en tiempo de ejecución | s2n o quiche; builds solo-Quinn lo ignoran |
max_ack_delay_ms | entero | 1 | |
request_body_drain_cap_bytes | entero | 65 536 | |
qlog | bool | false | Trazas qlog |
[http3]
enabled = true
provider = "s2n"
[[server]]
listen = "0.0.0.0:443"
http3_listen = "0.0.0.0:443"
server_name = ["exyonq.dev"]
routes = ["site"]
[server.tls]
cert = "/etc/exyonq/tls/exyonq.dev.pem"
key = "/etc/exyonq/tls/exyonq.dev.key"
http3_listen no se puede combinar con strict_sni, protocols, ciphers, curves, ocsp_stapling ni must_staple, ni con varios [[server]] TLS en el mismo listen: los proveedores QUIC solo consumen cert y key, y el producto prefiere rechazar la configuración antes que ignorarla.
[[map]]
Tabla de equivalencias exactas. Tope de 32 mapas y 64 entradas por mapa.
| Campo | Tipo | Defecto | Notas |
|---|
name | string | obligatorio | Se referencia como $nombre |
input | string | obligatorio | header, host, remote_addr, request_uri, scheme |
header | string | — | Obligatorio si input = "header" |
default | string | "" | Valor cuando no hay coincidencia |
values | tabla | vacía | Claves exactas |
[[map]]
name = "connection_upgrade"
input = "header"
header = "Upgrade"
default = "close"
values = { "websocket" = "upgrade" }
[[route]]
name = "ws"
match = { path = "/socket" }
upstream = "app"
proxy_set_headers = [
{ name = "Connection", value = "$connection_upgrade" },
{ name = "Host", value = "$host" },
]
Las variables disponibles en plantillas son exactamente $host, $remote_addr, $request_uri, $scheme y $<map>. Cualquier otro $… es error de configuración.
[[cache_policy]]
| Campo | Tipo | Defecto | Notas |
|---|
name | string | obligatorio | Lo referencia cache en la ruta |
ttl_seconds | entero | 30 | |
max_object_bytes | entero | 1 048 576 | |
[[cache_policy]]
name = "html"
ttl_seconds = 60
[[route]]
name = "site"
match = { path = "/" }
upstream = "app"
cache = "html"