Skip to content

Troubleshooting & FAQ

Use this page to diagnose the most common failure modes. Each section starts with the symptom, then the cause, then the fix.


error: unexpected token 'X' at line N

Run the formatter first — it surfaces token errors with precise line numbers:

Terminal window
dwaar fmt --check Dwaarfile
dwaar fmt Dwaarfile # auto-fix whitespace and ordering

Common causes:

SymptomCauseFix
unexpected token '{' after a directiveMissing space before blockAdd a space: reverse_proxy upstream:port {
unknown directive 'X'Directive not supported or misspelledCheck the directive reference
expected address, got 'X'Site address missing port or schemeUse example.com, :8080, or https://example.com
duplicate site blockTwo blocks with the same addressMerge into one site block
Config file not foundWrong path passed to --configPass the absolute path: dwaar run --config /etc/dwaar/Dwaarfile

dwaar fmt only checks syntax. Semantic errors (e.g. referencing an upstream that does not resolve) are caught at runtime. Run:

Terminal window
dwaar validate Dwaarfile

This performs full compilation including upstream DNS resolution and TLS config checks without starting the server.

Check the admin API reload endpoint returned 200 OK:

Terminal window
curl -s -X POST http://localhost:2019/reload | jq .

If it returned an error, the new config failed validation. The previous config remains active. Fix the Dwaarfile and reload again.


Certificate not provisioning (ACME / Let’s Encrypt)

Section titled “Certificate not provisioning (ACME / Let’s Encrypt)”

Port 80 must be reachable from the internet. ACME HTTP-01 challenges require Let’s Encrypt to reach http://<your-domain>/.well-known/acme-challenge/<token>.

Checklist:

  1. Confirm port 80 is open in your firewall / security group.
  2. Confirm DNS A/AAAA record points to this machine.
  3. Confirm no other process holds port 80 (ss -tlnp | grep :80).
  4. Check Dwaar logs for ACME error lines:
    Terminal window
    journalctl -u dwaar --since "5 minutes ago" | grep -i acme

Let’s Encrypt enforces rate limits: 5 duplicate certificates per week, 50 certificates per registered domain per week.

  • During development, point acme_ca at Let’s Encrypt Staging:
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
  • Staging issues untrusted certificates but has relaxed rate limits.
  • Switch back to production once the setup is confirmed working.

TLS handshake fails / “SSL_ERROR_RX_RECORD_TOO_LONG”

Section titled “TLS handshake fails / “SSL_ERROR_RX_RECORD_TOO_LONG””

The client is connecting with plain HTTP to a TLS port. Confirm the site block uses HTTPS:

https://example.com {
reverse_proxy backend:8080
}

Plain example.com without a scheme listens on both :80 and :443. If you see this error, the client is sending HTTP to :443.

Self-signed certificate warnings in browser

Section titled “Self-signed certificate warnings in browser”

If you did not configure tls or acme_ca and Dwaar cannot reach Let’s Encrypt, it falls back to a self-signed certificate. Add a valid tls directive pointing to your cert files, or ensure port 80 is reachable for ACME.

Dwaar renews certificates automatically when they have fewer than 30 days remaining. If renewal is failing:

  1. Check that port 80 is still reachable (firewall rules change after reboots on some cloud providers).
  2. Check logs for renewal failed messages.
  3. Force a manual renewal via the admin API:
    Terminal window
    curl -X POST http://localhost:2019/certs/renew

Dwaar reached the upstream but received no valid response. Common causes:

CauseDiagnosisFix
Upstream not runningcurl http://upstream:port/ from Dwaar hostStart the upstream service
Wrong upstream addressCheck reverse_proxy directiveCorrect host/port
Upstream TLS mismatchUpstream expects TLS, Dwaar sends plainAdd transport http { tls } block
Upstream returned non-HTTP dataCheck upstream logsFix upstream application error
error: connect ECONNREFUSED 127.0.0.1:8080

The upstream is not listening. Confirm:

Terminal window
ss -tlnp | grep 8080 # is something listening?
curl -v http://127.0.0.1:8080 # can Dwaar's host reach it?

If running in Docker, 127.0.0.1 inside the Dwaar container refers to the container itself, not the host. Use the service name (backend:8080) or host.docker.internal:8080.

The upstream accepted the connection but did not respond within the deadline. Check:

  1. Upstream is healthy (curl http://upstream/health).
  2. Upstream is not under load — check its CPU and queue depth.
  3. Adjust timeouts in the Dwaarfile if the upstream is legitimately slow:
    reverse_proxy backend:8080 {
    transport http {
    read_timeout 120s
    dial_timeout 10s
    }
    }

WebSocket connections drop after 60 seconds

Section titled “WebSocket connections drop after 60 seconds”

Pingora’s default downstream keepalive is 60 seconds. WebSocket connections need a longer timeout. Set:

reverse_proxy ws-backend:8080 {
transport http {
keepalive_timeout 300s
}
}

Expected memory footprint:

ConfigurationApproximate RSS
Single site, no analytics~5 MB
100 sites, analytics on~6 MB
Per active connection~64 KB
Per cached response (depends on body size)variable

If RSS is significantly higher:

  1. Check whether HTTP cache is enabled with no max_size bound. Set an explicit limit:
    cache {
    max_size 512mb
    }
  2. Check for memory growth over time (leak) by watching RSS over hours. If it grows continuously, file a bug with a heap profile from jemalloc’s built-in stats:
    Terminal window
    MALLOC_CONF=stats_print:true dwaar run --config Dwaarfile 2>&1 | grep -A 20 "jemalloc stats"

Steps to isolate:

  1. Disable plugins one at a time — bot detection regex and rate limiting add CPU cost per request.
  2. Check upstream latency — compare upstream_latency_ms in request logs against wall time.
  3. Enable Prometheus metrics and check dwaar_request_duration_seconds{quantile="0.99"}.
  4. Profile — see the Profiling section.

Config reload should complete in under 10 ms. If it is slow, the Dwaarfile is large and regex compilation is the bottleneck. Split large sites into separate files using import:

import /etc/dwaar/sites/*.dwaar

Error response from daemon: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

Dwaar’s Docker integration reads the socket to discover container labels. Fix:

Terminal window
# Option 1: add the dwaar user to the docker group
sudo usermod -aG docker dwaar
# Option 2: mount the socket with the correct permissions in compose
services:
dwaar:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
user: "0" # run as root only if absolutely necessary

Dwaar discovers containers via labels. Confirm the container has the required labels:

labels:
dwaar.enable: "true"
dwaar.host: "myapp.example.com"
dwaar.port: "8080"

Then check that label discovery is enabled in the Dwaarfile:

docker {
label_prefix dwaar
}

Restart Dwaar after adding labels — label discovery runs at startup and on reload, not continuously.

Docker reassigns container IPs on restart. Dwaar resolves upstreams by DNS name, not IP. Use the Docker service name as the upstream address:

reverse_proxy myapp:8080

Docker’s embedded DNS resolves service names to the current container IP.


Ingress resource not translating to routes

Section titled “Ingress resource not translating to routes”

Check that the IngressClass matches:

spec:
ingressClassName: dwaar

Confirm the dwaar-ingress controller has the correct RBAC permissions:

Terminal window
kubectl auth can-i list ingresses --as=system:serviceaccount:dwaar:dwaar-ingress -n your-namespace

If RBAC is missing, apply the ClusterRole from the Helm chart:

Terminal window
helm upgrade dwaar dwaar/dwaar-ingress --set rbac.create=true

Dwaar’s Kubernetes controller uses a Lease resource for leader election. If no pod becomes leader:

Terminal window
kubectl get lease -n dwaar
kubectl describe lease dwaar-leader -n dwaar

Common causes:

SymptomCauseFix
Lease not foundRBAC missing leases permissionRe-apply Helm chart with rbac.create=true
Leader not changing after pod deathLease TTL not expiredWait for TTL (default 15s) or delete the Lease manually
All pods in CrashLoopBackOffAPI server unreachableCheck network policy and kube-apiserver health

The reflector watches for Ingress events. If updates are not propagating:

Terminal window
kubectl logs -n dwaar -l app=dwaar-ingress --tail=50 | grep -i reconcil

Look for reconcile error lines. Common cause: the Ingress references a Service that does not exist. Create the Service or remove the backend reference.


By default, the admin API listens on a Unix domain socket at /run/dwaar/admin.sock. It does not bind a TCP port unless configured.

Connect via the socket:

Terminal window
curl --unix-socket /run/dwaar/admin.sock http://localhost/routes

Or configure a TCP listener in the Dwaarfile:

admin :2019 {
origins localhost
}

The admin API uses bearer token authentication. Pass the token from your Dwaarfile:

Terminal window
curl -H "Authorization: Bearer <your-token>" \
--unix-socket /run/dwaar/admin.sock \
http://localhost/routes

If you lost the token, it is in the admin block of your Dwaarfile. Rotate it with a reload.

The request originated from an IP not in the origins allowlist. Either connect via the Unix socket (always allowed) or add your IP to the origins list:

admin :2019 {
origins 127.0.0.1 10.0.0.0/8
}

Is Dwaar production-ready?

Dwaar has completed 27 build phases covering TLS automation, HTTP/3, WebSocket proxying, gRPC, mTLS, Kubernetes ingress, WASM plugins, an observability pipeline, and 900+ passing tests. The core proxy engine is built on Cloudflare’s battle-tested Pingora framework. It is suitable for production use for teams comfortable running early-stage software and willing to track the changelog.

How does it compare to nginx?

FeatureDwaarnginx
Configuration languageCaddyfile-style (Dwaarfile)nginx.conf
Automatic TLSYes (ACME, DNS-01)With certbot / OpenResty
Hot reloadYes, sub-10msYes (graceful reload)
HTTP/3 / QUICYesnginx Plus or community patch
Built-in analyticsYes (JS beacon, Web Vitals)No
WASM pluginsYesNo
Memory footprint~5 MB base~5–15 MB base
Allocatorjemalloc (no fragmentation)libc malloc
Config language featuresMatchers, templates, variablesLocation blocks, if (limited)

Dwaar is not a drop-in replacement for nginx — it uses a different configuration model. See the migration guide for a directive mapping.

Can I use Dwaar with Cloudflare?

Yes. Place Dwaar behind Cloudflare’s CDN as the origin server. For end-to-end TLS, set Cloudflare’s SSL mode to Full (strict) and point the origin to port 443. Dwaar handles ACME certificates independently of Cloudflare.

Dwaar also supports Cloudflare’s DNS-01 ACME provider for wildcard certificates:

tls {
dns cloudflare {env.CF_API_TOKEN}
}

What is the license?

Dwaar is licensed under the Business Source License 1.1 (BSL-1.1). The change license is AGPL-3.0, effective ten years after each release. You can use Dwaar freely for any purpose except offering a competing commercial proxy, CDN, or analytics service. See LICENSE in the repository root.

Does Dwaar support HTTP/2?

Yes. HTTP/2 is enabled automatically for TLS sites. Downstream HTTP/2 multiplexing is handled by Pingora. Upstream connections use HTTP/1.1 by default; configure HTTP/2 upstream with:

reverse_proxy backend:8080 {
transport http {
versions h2
}
}

Can I run multiple workers?

Yes. Pass --workers N to fork N worker processes before Pingora initializes. Each worker runs an independent tokio runtime. The supervisor process restarts crashed workers automatically.

Terminal window
dwaar run --config Dwaarfile --workers 4

  • GitHub Issuesgithub.com/permanu/dwaar/issues. Search before opening a new issue. Include your Dwaar version (dwaar --version), OS, and a minimal reproducible Dwaarfile.
  • Discussions — Use GitHub Discussions for questions that are not bugs.
  • Security vulnerabilities — Do not open a public issue. Email the address in SECURITY.md.

When reporting a bug, attach:

  1. The output of dwaar --version
  2. Your Dwaarfile (redact secrets)
  3. The relevant log lines (journalctl -u dwaar --since "10 minutes ago")
  4. Steps to reproduce