Skip to content

tternquist/beyond-ads-dns

Repository files navigation

BEYOND-ADS-DNS Logo

beyond-ads-dns

Ad-blocking DNS resolver that uses public blocklists (e.g. Hagezi) and Redis caching to reduce upstream traffic. Performance optimized.

AI-Human Collaboration: This repository is an AI-assisted project. While AI was used to generate the boilerplate and initial code implementation, the product roadmap, system design, and overall architecture were human-directed. We believe in leveraging AI for efficiency while maintaining human oversight for quality and intent.

Running the application

Recommended: Docker Compose. The easiest way to run beyond-ads-dns is with one of the Docker Compose examples. They include Redis, ClickHouse, and the Metrics UI—no manual setup required.

Example Summary Link
Basic Minimal deployment using the published GHCR image (no build). Redis, ClickHouse, Metrics UI. examples/basic-docker-compose/
Appliance + Watchtower For devices outside your control. Uses appliance tag; Watchtower auto-updates when you promote a new version. examples/appliance-with-watchtower/
Let's Encrypt Automatic HTTPS for the Metrics UI via Let's Encrypt. examples/letsencrypt-docker-compose/
Grafana Adds Prometheus and Grafana for monitoring, dashboards, and query analytics. examples/grafana-integration/
Max Performance Tuned for high throughput (2GB Redis, 100K L0 cache, higher batch sizes). examples/max-performance-docker-compose/
Raspberry Pi MicroSD-friendly: ClickHouse in memory only (tmpfs), tmpfs for Redis and logs. examples/raspberry-pi-docker-compose/
Unbound Unbound as recursive upstream—no third-party DNS, full DNSSEC validation. examples/unbound-docker-compose/
Redis Sentinel Learning: Redis HA with master-replica and automatic failover. examples/redis-sentinel-docker-compose/
Redis Cluster Learning: Redis Cluster with sharded data and failover. examples/redis-cluster-docker-compose/
Source build Build image from source (not standard—use only for custom code, dev, or restricted environments). examples/source-build-docker-compose/
Kubernetes (Helm) Deploy on Kubernetes with optional NodePort or hostNetwork for DNS. Redis/ClickHouse external or via dependencies. helm/beyond-ads-dns/

Quick start (Basic example):

cd examples/basic-docker-compose
docker compose up -d

Alternative: Non-Docker installation

If you cannot use Docker, you can run the resolver and its dependencies manually.

Prerequisites:

  • Go 1.24+ (to build the binary)
  • Redis (required for DNS cache)
  • ClickHouse (optional; disable with query_store.enabled: false in config)

1. Build the binary:

go build -o beyond-ads-dns ./cmd/beyond-ads-dns

2. Create and edit config:

cp config/config.example.yaml config/config.yaml

Edit config/config.yaml and set addresses for your local services:

  • cache.redis.address: Redis address (e.g. localhost:6379)
  • cache.redis.password: Redis password if your Redis uses requirepass (see Redis password setup)
  • query_store.address: ClickHouse HTTP address (e.g. http://localhost:8123) if using query store
  • query_store.username / query_store.password: ClickHouse credentials (prefer QUERY_STORE_PASSWORD env; see ClickHouse password setup)

If you disable the query store, set query_store.enabled: false and omit ClickHouse setup.

3. Set up Redis and ClickHouse (if needed):

  • Redis: Install Redis (e.g. apt install redis-server on Debian/Ubuntu) and ensure it is running.
  • ClickHouse: Install ClickHouse and create the schema from db/clickhouse/init.sql. Default credentials in the example config are beyondads/beyondads.

4. Run the resolver:

./beyond-ads-dns -config config/config.yaml

The Metrics UI (React app + Node.js API) is bundled in the Docker image. For non-Docker setups, run it separately—see the Metrics UI section.

Kubernetes (Helm)

To run on Kubernetes, use the included Helm chart. You must provide a Redis URL; ClickHouse is optional.

helm install beyond-ads-dns ./helm/beyond-ads-dns --set redis.url=redis://your-redis:6379

DNS can be exposed via NodePort (e.g. <node-ip>:3053) or hostNetwork (port 53 on the node). See helm/beyond-ads-dns/README.md for configuration, persistence, and optional ClickHouse init.

Architecture (data structures + algorithms)

Blocklist compilation and matching

  • Sources: blocklists are fetched on a schedule and parsed line‑by‑line.
  • Normalization: each line is trimmed, comments removed, and common list formats are supported (hosts file lines, ||domain^ rules, AdBlock-style with $important, $script, |https://domain^, etc). Domains are lower‑cased, trailing dots removed, and *. stripped.
  • Storage: entries are stored in an in‑memory hash set map[string]struct{} for O(1) lookups.
  • Overrides:
    • allowlist entries are stored in a separate set and always win.
    • denylist entries are always blocked, even if not in blocklists.
  • Matching algorithm: the query name is normalized and checked for suffix matches by progressively stripping left‑most labels (ads.example.comexample.comcom). This allows a single list entry to match subdomains efficiently.

Cache layout and refresh

  • Cache key: dns:<qname>:<qtype>:<qclass>
  • Value: a Redis hash containing:
    • msg: wire‑encoded DNS response
    • soft_expiry: UNIX epoch for the soft TTL
  • Expiry index: a sorted set dnsmeta:expiry:index keyed by soft_expiry to enable sweep scans.
  • Metadata: hit counters and refresh locks use the dnsmeta: prefix and may expire; cache entries do not.
  • Redis eviction policy: By default, Redis uses allkeys-lru (Least Recently Used) to evict keys when maxmemory is reached. This ensures that less-frequently accessed DNS entries are removed first when memory pressure occurs. Cache entries persist without Redis TTLs, so eviction is the only way they are removed from Redis (besides explicit deletes). Redis is configured via config/redis.conf (see below for details).
  • Refresh algorithms:
    • Refresh‑ahead: on cache hit, refresh if soft TTL is below min_ttl or hot_ttl (for hot keys).
    • Sweeper: periodically scans the expiry index and refreshes keys close to expiry and with at least sweep_min_hits within sweep_hit_window.
  • Stale serving: expired entries can be served for stale_ttl while refresh runs in the background.

Query store and metrics

  • ClickHouse storage: each query is inserted as a row with timestamp, client, qname, outcome, and latency. This powers query dashboards.

  • Response time measurement: The duration_ms metric measures the complete end-to-end response time from when the query is received until the response is written back to the client. This includes:

    • Blocklist checking (if enabled)
    • Cache lookup time (Redis query)
    • Upstream DNS query time (if cache miss)
    • Cache write time (for new entries)
    • Network time to send the response to the client

    This is not just the upstream DNS response time—it captures the full request processing latency, giving you a complete picture of client experience.

  • Metrics API: the Node.js API exposes Redis stats, query summaries, and refresh sweep stats to the UI.

Control plane

  • Control server: /blocklists/reload applies config changes by reloading blocklists without restarting the DNS service.

Usage and configuration

The resolver loads a default config from config/default.yaml and then applies any overrides found in config/config.yaml (gitignored). You can override the user config path with -config or CONFIG_PATH, and the default path with DEFAULT_CONFIG_PATH.

Redis address via environment: REDIS_ADDRESS or REDIS_URL overrides cache.redis.address. Use REDIS_ADDRESS for host:port (e.g. redis:6379) or REDIS_URL for a full URL (e.g. redis://redis-node-1:6379). Useful in Docker when the Redis hostname differs from the default.

Create a user override config to customize blocklists and upstreams:

cp config/config.example.yaml config/config.yaml

Config overview

server:
  listen: ["0.0.0.0:53"]
  protocols: ["udp", "tcp"]
  read_timeout: "5s"
  write_timeout: "5s"

# Upstreams: plain DNS (host:port), DoT (tls://host:853), DoQ (quic://host:853), or DoH (https://host/dns-query)
upstreams:
  - name: cloudflare
    address: "1.1.1.1:53"

blocklists:
  refresh_interval: "6h"
  sources:
    - name: hagezi-pro
      url: "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/domains/pro.txt"
  allowlist: []
  denylist: []

cache:
  redis:
    address: "redis:6379"
    db: 0
    password: ""
  min_ttl: "300s"
  max_ttl: "1h"
  negative_ttl: "5m"
  # client_ttl_cap: "5m"   # Two-tier TTL: max TTL in client responses when serving from cache. Omit = disabled. Default 5m.
  refresh:
    enabled: true
    hit_window: "1m"
    hot_threshold: 20
    min_ttl: "30s"
    hot_ttl: "2m"
    serve_stale: true
    stale_ttl: "1h"
    lock_ttl: "10s"
    max_inflight: 100
    sweep_interval: "15s"
    sweep_window: "1m"
    max_batch_size: 2000
    sweep_min_hits: 1
    sweep_hit_window: "48h"

response:
  blocked: "nxdomain"   # or IP (e.g. resolver IP) for block page; set to server IP to serve block page for blocked domains
  blocked_ttl: "1h"

request_log:
  enabled: false
  directory: "logs"
  filename_prefix: "dns-requests"
  format: "text"  # or "json" for structured logs with query_id

# Client identification: map IPs to names for per-device analytics (Configure → Clients)
# client_identification:
#   enabled: true
#   clients:
#     - ip: "192.168.1.10"
#       name: "Kids Tablet"
#       group_id: "kids"
# client_groups:
#   - id: "kids"
#     name: "Kids"
#     description: "Children's devices"

query_store:
  enabled: true
  address: "http://clickhouse:8123"
  database: "beyond_ads"
  table: "dns_queries"
  username: "beyondads"
  password: "beyondads"
  flush_to_store_interval: "5s"   # How often the app sends buffered events to ClickHouse
  flush_to_disk_interval: "5s"   # How often ClickHouse flushes async inserts to disk
  batch_size: 2000
  sample_rate: 1.0  # Fraction to record (0.0-1.0). Use <1.0 to reduce load at scale.

control:
  enabled: true
  listen: "0.0.0.0:8081"
  token: ""

# Multi-instance sync (optional). Configure via Metrics UI Sync tab or manually:
# sync:
#   enabled: true
#   role: primary   # or replica
#   # Primary: tokens for replicas (created via UI)
#   tokens: []
#   # Replica only:
#   primary_url: "http://primary-host:8081"
#   sync_token: "token-from-primary"
#   sync_interval: "60s"

Request logging is disabled by default. Set request_log.enabled: true to enable daily rotation. Use request_log.format: "json" for structured JSON logs with query_id, qname, outcome, and duration_ms. Query store supports query_store.sample_rate (0.0–1.0) to record a fraction of queries, reducing load at scale. Set query_store.anonymize_client_ip: "hash" or "truncate" for GDPR/privacy-compliant retention.

Cache refresh-ahead is enabled by default. The resolver will preemptively refresh hot entries when they are close to expiring. Tune cache.refresh.* to adjust how aggressive the refresh should be. The sweeper periodically scans for keys nearing expiration and refreshes them, even if they are not actively requested. Stale serving keeps expired entries available for cache.refresh.stale_ttl while background refreshes keep them up to date. Cache entries are stored without Redis TTLs; soft expiry is tracked internally so keys persist until Redis evicts them based on the configured eviction policy (default: allkeys-lru). Metadata keys (hit counters, locks, sweep index) use the dnsmeta: prefix and may have TTLs; cache entries keep the dns: prefix and do not expire.

Redis configuration

Redis is configured via config/redis.conf, which is mounted into the Redis container. The default configuration sets:

  • maxmemory 512mb: Memory limit before eviction
  • maxmemory-policy allkeys-lru: Eviction policy
  • Persistence: RDB snapshot at most every 5 minutes (save 300 1); AOF disabled (DNS cache repopulates quickly on restart)

Available eviction policies:

  • allkeys-lru (default): Evict least recently used keys from all keys
  • allkeys-lfu: Evict least frequently used keys from all keys
  • allkeys-random: Evict random keys from all keys
  • volatile-lru: Evict least recently used keys with TTL set
  • volatile-lfu: Evict least frequently used keys with TTL set
  • volatile-random: Evict random keys with TTL set
  • volatile-ttl: Evict keys with TTL set, shortest TTL first
  • noeviction: Return errors when memory limit is reached

Since DNS cache entries do not have Redis TTLs, volatile-* policies will only evict metadata keys (hit counters, locks). For typical DNS caching, allkeys-lru or allkeys-lfu are recommended to ensure cache entries can be evicted under memory pressure.

To customize Redis settings, edit config/redis.conf before starting the containers.

For production HA, use Redis Sentinel or Cluster:

cache:
  redis:
    mode: sentinel
    master_name: mymaster
    sentinel_addrs: ["sentinel1:26379", "sentinel2:26379"]
    password: ""
    lru_size: 10000

Or Redis Cluster: mode: cluster with cluster_addrs: ["node1:6379", "node2:6379", ...].

Docker users can pass Redis config via environment variables (no config file needed):

  • Standalone: REDIS_URL or REDIS_ADDRESS
  • Sentinel: REDIS_MODE=sentinel, REDIS_MASTER_NAME=mymaster, REDIS_SENTINEL_ADDRS=sentinel1:26379,sentinel2:26379 (or REDIS_ADDRESS as comma-separated sentinels)
  • Cluster: REDIS_MODE=cluster, REDIS_CLUSTER_ADDRS=node1:6379,node2:6379,node3:6379 (or REDIS_ADDRESS as comma-separated nodes)

Cache refresh details

The cache keeps two notions of expiry:

  • Soft expiry: the original DNS TTL (after clamping). This decides whether a response is "fresh" or "stale".
  • Redis eviction: keys do not have Redis TTLs. They remain until Redis evicts them based on its configured policy/memory limits.

Refresh paths

There are two refresh mechanisms that can run together:

  1. Request‑driven refresh (refresh‑ahead)
    When a cached entry is served and its soft TTL is low, the resolver refreshes it in the background. The threshold is based on recent request frequency:

    • If the entry has query rate ≥ hot_threshold_rate (queries per minute) within hit_window, it is treated as "hot" and refreshed once its TTL is low. When client_ttl_cap is set (default 5m), the default adapts: ~3 clients = hot (2/min for 5m cap); single client stays warm. Without client cap, 20/min. soft TTL is below hot_ttl.
    • Otherwise, it refreshes once soft TTL is below min_ttl.
  2. Periodic sweeper
    The sweeper runs every sweep_interval, scanning the internal soft‑expiry index for keys expiring within sweep_window. It schedules refreshes for keys that are close to expiry and that have seen at least sweep_min_hits within sweep_hit_window. Hits are recorded on cache serves and on successful cache writes after upstream responses.

    What happens to entries that don't meet the threshold?
    Entries with fewer than sweep_min_hits are skipped by the sweeper and are not proactively refreshed. They remain in cache and will:

    • Continue to be served if still fresh (before soft expiry)
    • Be served as stale if serve_stale is enabled (within stale_ttl after soft expiry)
    • Become unservable after stale_ttl expires (hard cache miss)
    • Persist in Redis until evicted by Redis's memory policy, since cache entries do not have Redis TTLs
    • Still be eligible for request‑driven refresh if accessed by a client

Both refresh paths are protected by a distributed lock (per key) and a local inflight limit, so a single hot key won’t trigger stampedes.

Stale serving

If serve_stale is enabled, the resolver will serve expired entries for up to stale_ttl after soft expiry, while a refresh is scheduled in the background. This avoids a hard cache miss for clients when the entry has just gone stale.

Configuration reference

cache:
  refresh:
    enabled: true          # Master switch for refresh-ahead + sweeper
    hit_window: "1m"       # Window for counting request frequency
    hot_threshold: 20      # Absolute fallback when hot_threshold_rate is 0
    # hot_threshold_rate: auto when client_ttl_cap set (~2/min for 5m cap; single client stays warm); else 20/min
    min_ttl: "1h"          # Refresh threshold for non-hot entries
    hot_ttl: "2m"          # Refresh threshold for hot entries
    serve_stale: true      # Serve expired entries within stale_ttl
    stale_ttl: "1h"        # Max time to serve stale entries
    expired_entry_ttl: "30s"  # TTL in DNS response when serving expired entries
    lock_ttl: "10s"        # Per-key refresh lock in Redis
    max_inflight: 100       # Max concurrent refreshes per instance
    sweep_interval: "15s"  # How often the sweeper runs
    sweep_window: "1m"     # How far ahead the sweeper scans
    max_batch_size: 2000    # Max keys processed per sweep
    sweep_min_hits: 1      # Min hits in sweep_hit_window to refresh
    sweep_hit_window: "48h" # Time window for sweep_min_hits

Tuning guidance

  • More aggressive refresh: increase hot_ttl/min_ttl, shorten sweep_interval, or increase sweep_window.
  • Less upstream load: decrease hot_ttl/min_ttl and increase hit_window or hot_threshold.
  • Avoid stampedes: keep lock_ttl >= expected upstream latency and set max_inflight to a reasonable limit for your instance.

Query storage uses ClickHouse and is enabled by default. Set query_store.enabled: false to disable it. The ClickHouse schema lives in db/clickhouse/init.sql. It uses partition-level TTL to reduce disk writes; existing installations should run the migration in db/clickhouse/PARTITION_TTL_MIGRATION.md. The default Docker Compose credentials are beyondads/beyondads.

Query store flush intervals:

  • flush_to_store_interval (default 5s): How often the app sends buffered query events to ClickHouse. Also triggers when batch_size is reached.
  • flush_to_disk_interval (default 5s): How often ClickHouse flushes its async insert buffer to disk (async_insert_busy_timeout_ms). Controls when data is durably persisted.

The Docker Compose examples disable ClickHouse's internal system logs (query_log, part_log, etc.) to reduce disk writes. If you see persistent SystemLogFlush or MergeMutate writes in iotop, see docs/clickhouse-disk-writes.md.

The control server is used by the UI to apply blocklist changes. If you set control.token, the UI must send the same token via DNS_CONTROL_TOKEN in the metrics API.

DoH/DoT server (encrypted client connections)

To accept DNS-over-HTTPS and DNS-over-TLS from clients, enable the DoH/DoT server with TLS certificates:

doh_dot_server:
  enabled: true
  cert_file: "/path/to/fullchain.pem"
  key_file: "/path/to/privkey.pem"
  dot_listen: "0.0.0.0:853"   # DoT (DNS over TLS)
  doh_listen: "0.0.0.0:8443"  # DoH (DNS over HTTPS); use 443 if not sharing with web UI
  doh_path: "/dns-query"

Clients can then use tls://your-host:853 for DoT or https://your-host:8443/dns-query for DoH.

Block page

When response.blocked is set to your resolver's IP (e.g. the host running the Metrics UI), blocked domains resolve to that IP. The Metrics UI serves a simple HTML block page when a browser requests a blocked domain. Configure response.blocked to your server's IP and ensure DNS points clients to your resolver.

Performance

The resolver uses a multi-tier caching architecture for maximum performance:

  • L0 Cache: In-memory LRU cache (~10-50μs latency)
  • L1 Cache: Redis distributed cache (~0.5-2ms latency)
  • Bloom Filter: Fast negative lookups for blocklists
  • Refresh-Ahead: Proactive cache refresh to avoid expiry
  • Optimized Connection Pools: Redis pool with 50 connections

Expected performance with default configuration:

  • Hot queries: <0.1ms latency, 500K-1M QPS per instance
  • Cached queries: 0.5-2ms latency, 50K-100K QPS per instance
  • Cache hit rate: 95-99% in production

See docs/performance.md for detailed performance documentation and tuning guide. For metered or low-bandwidth deployments, see Network Bandwidth Configuration for how to balance configuration options and reduce network usage.

Testing

Automated tests include Go unit tests, web server (Node.js) tests, and web client (Vitest) tests. All run in CI on every push and pull request.

go test ./...                           # Go tests
npm test --prefix web/server            # Web server API tests
npm test --prefix web/client            # Web client unit tests

See docs/testing.md for full documentation: test locations, coverage, and how to add new tests. For an overview of architecture and code practices (for AI agents and developers), see docs/code-and-architecture-standards.md.

Docker

The Docker image combines the DNS resolver and metrics API in a single container. Redis and ClickHouse run as separate services. No config files are required—the image includes sensible defaults (Hagezi blocklist, Cloudflare upstreams).

To build the image from source (e.g. for custom deployments), use the source-build Docker Compose example or run docker build -t beyond-ads-dns . from the repo root. This is not the standard approach—for most users, use one of the other Docker Compose examples above; they use the published image from GitHub Container Registry and require no build.

Image tags: stable (manually promoted, recommended for production), appliance (for Watchtower-monitored deployments—promote validated releases via Promote to Appliance Tag), latest (auto-updates with each release), v1.2.3 (pinned version), edge (bleeding-edge from main). Use the Promote to Stable Tag workflow to selectively promote a validated release to stable.

To customize blocklists or upstreams, use the Metrics UI—changes save to ./config/config.yaml on the host. Default config is in the image; no default.yaml required. Set HOSTNAME in .env or configure hostname in Settings → UI to customize the hostname shown in the UI. The Settings hostname overrides HOSTNAME when both are set.

Metrics UI

The metrics UI is a React app backed by a Node.js API, bundled in the same Docker image as the DNS resolver. It surfaces Redis cache statistics, recent query rows, blocklist management, instance sync configuration, and the active configuration (when the control server is enabled). The query table supports filtering, pagination, sorting, and CSV export. Configure → Local Records provides a dedicated, Route53-inspired interface for managing local DNS records (table view, filtering, record details panel).

Run via one of the Docker Compose examples (recommended; e.g. examples/basic-docker-compose). Visit http://localhost for the Metrics UI.

For non-Docker setups, run the web server and client separately:

cd web/server && npm install && npm run dev
cd web/client && npm install && npm run dev

Instance Sync

The Metrics UI includes a Sync tab to configure multi-instance sync. You can enable sync, choose a role (primary or replica), and manage settings entirely from the UI—no manual config editing required.

Primary instance (source of truth):

  • Create sync tokens for replicas to authenticate
  • Revoke tokens when replicas are decommissioned
  • Blocklists, upstreams, and local records are managed here and pushed to replicas

Replica instance:

  • Set the primary URL (e.g. http://primary-host:8081)
  • Enter the sync token from the primary
  • Configure sync interval (e.g. 60s, 5m)
  • DNS-affecting config (blocklists, upstreams, local records) is read-only; it is synced from the primary

To get started: open the Sync tab, choose Primary or Replica, and follow the prompts. Restart the application after saving to apply changes.

Settings

The Settings tab configures server, cache, query store, control, logging, and request log. Key options:

  • Application Logging — Format (text/JSON) and level (error/warning/info/debug). Use JSON for Grafana/Loki integration.
  • Control → Error persistence — Persist errors to disk for the Error Viewer.
  • Request Log — Log DNS requests to disk (text or JSON).
  • Clients & Groups — Map client IPs to friendly names and assign to groups for per-device analytics and future per-group blocklists (parental controls). Manage via Configure → Clients. Applies immediately, no restart. See Clients and Groups.

Most settings require a restart to take effect.

Grafana Integration

The resolver exposes Prometheus metrics at http://localhost:8081/metrics when the control server is enabled. To run with Grafana and Prometheus, use the Grafana integration example:

cd examples/grafana-integration
docker compose up --build

Then open Grafana at http://localhost:3000 (login: admin / admin). Prometheus, ClickHouse, and Loki datasources are pre-configured. See examples/grafana-integration/README.md for details. For application logs in Grafana, set Settings → Application Logging → Format to JSON in the Metrics UI and restart.

Local development:

cd web/server && npm install && npm run dev
cd web/client && npm install && npm run dev

Security

The metrics UI supports optional login, sessions, and HTTPS.

User/Password login

When a password is configured, the UI requires login before accessing any data.

Set password via environment variable:

# In your docker-compose.yml (e.g. examples/basic-docker-compose/docker-compose.yml)
environment:
  - UI_PASSWORD=your-secure-password

Or use ADMIN_PASSWORD (alias) or UI_USERNAME to customize the admin username (default: admin).

Set password via command in container:

docker exec beyond-ads-dns beyond-ads-dns set-admin-password your-secure-password

Or run interactively (prompts for password):

docker exec -it beyond-ads-dns beyond-ads-dns set-admin-password

The command writes a bcrypt hash to /app/config-overrides/.admin-password. Override the path with ADMIN_PASSWORD_FILE.

Set or change password via UI:

When the password is stored in a file (not set via UI_PASSWORD or ADMIN_PASSWORD env), you can set or change it from System Settings in the web UI:

  • Initial setup: If no password is configured, go to System Settings and set a password. The UI will reload and require login.
  • Change password: When logged in, go to System Settings, enter your current password and the new password, then click "Change password".

If the password is configured via environment variables, it cannot be changed from the UI.

Initial setup and persistence:

  • By default, new installations have no password. Set one via the UI or CLI before exposing the dashboard on a shared network.
  • When using file-based auth, the password file must be on a persistent volume. In Docker Compose examples, ./config:/app/config-overrides ensures the password persists across container restarts. Without this volume, the password would be lost on restart.

Sessions

Sessions are stored in Redis with a configurable secret. Set SESSION_SECRET in production for stable session signing. Cookie is httpOnly, sameSite=lax, and secure when HTTPS is enabled.

HTTPS

Option 1: Let's Encrypt (automatic)

For automatic HTTPS with free certificates from Let's Encrypt:

environment:
  - LETSENCRYPT_ENABLED=true
  - LETSENCRYPT_DOMAIN=your-domain.com
  - LETSENCRYPT_EMAIL=admin@your-domain.com

HTTP challenge (default): Port 80 must be publicly reachable. DNS challenge (alternative): Set LETSENCRYPT_CHALLENGE_TYPE=dns when port 80 is not available—add TXT records manually when prompted. Certificates are stored in a volume and auto-renew on startup when expiring. See examples/letsencrypt-docker-compose/ for a full example.

Option 2: Manual certificates

environment:
  - HTTPS_ENABLED=true
  - SSL_CERT_FILE=/path/to/cert.pem
  - SSL_KEY_FILE=/path/to/key.pem
  - HTTPS_PORT=443

Mount the certificate files into the container. Alternatively, use a reverse proxy (nginx, Traefik) for TLS termination.

Performance testing

Use the built-in harness to run large query bursts and optionally flush Redis between runs:

go run ./cmd/perf-tester -resolver 127.0.0.1:53 -flush-redis -control-url http://127.0.0.1:8081

See tools/perf/README.md for more options (warmup, TCP, custom name lists, etc).

Note on latency measurements: The performance tester measures client-side round-trip time (from sending query to receiving response, including network latency). This differs from the server-side duration_ms stored in ClickHouse, which measures end-to-end processing time within the resolver (see "Query store and metrics" section above for details).

About

Ad-blocking DNS resolver that uses public blocklists (e.g. Hagezi) and Redis caching to reduce upstream traffic. Performance optimized.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors