Skip to content

Traefik

Configure AdGuard Home, Traefik, Keepalived and Pocket ID for a self-hosted DNS proxy.


  • Directoryroot
    • compose.yml (*)
    • Directorytraefik
      • Directoryconfig
        • traefik.yml (*)
        • dynamic_config.yml (*)
      • Directoryletsencrypt
        • acme.json
    • Directoryadguard
      • Directoryconf
        • AdGuardHome.yaml (*)
      • Directorywork/
        • …
    • Directorykeepalived
      • keepalived.conf

  1. Terminal window
    mkdir -p {{DOCKER_PATH_VAR}}/dns-proxy/traefik/{config,letsencrypt} {{DOCKER_PATH_VAR}}/dns-proxy/pocket-id/data {{DOCKER_PATH_VAR}}/dns-proxy/adguard/{conf,work} {{DOCKER_PATH_VAR}}/dns-proxy/keepalived && cd {{DOCKER_PATH_VAR}}/traefik
    Terminal window
    nano compose.yml
    compose.yml
    services:
    traefik:
    container_name: traefik
    image: traefik:v3.7
    ports:
    - "80:80"
    - "443:443"
    environment:
    CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN}
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - ./config/traefik:/etc/traefik:ro
    - ./letsencrypt:/letsencrypt
    extra_hosts:
    - "host.docker.internal:host-gateway"
    networks:
    - proxy
    restart: unless-stopped
    pocket-id:
    container_name: pocket-id
    image: pocketid/pocket-id:v2
    env_file: .env
    ports:
    - "1411:1411"
    volumes:
    - "./pocket-id/data:/app/data"
    networks:
    - proxy
    restart: unless-stopped
    traefik-forward-auth:
    container_name: traefik-forward-auth
    image: ghcr.io/italypaleale/traefik-forward-auth:4
    environment:
    DEFAULT_PROVIDER: ${DEFAULT_PROVIDER}
    PROVIDERS_OIDC_ISSUER_URL: ${PROVIDERS_OIDC_ISSUER_URL}
    PROVIDERS_OIDC_CLIENT_ID: ${PROVIDERS_OIDC_CLIENT_ID}
    PROVIDERS_OIDC_CLIENT_SECRET: ${PROVIDERS_OIDC_CLIENT_SECRET}
    SECRET: ${SECRET}
    COOKIE_DOMAIN: ${COOKIE_DOMAIN}
    AUTH_HOST: ${AUTH_HOST}
    networks:
    - proxy
    restart: unless-stopped
    adguard:
    container_name: adguard
    image: adguard/adguardhome:latest
    restart: unless-stopped
    volumes:
    - ./adguard/work:/opt/adguardhome/work
    - ./adguard/conf:/opt/adguardhome/conf
    network_mode: host
    adguardhome-sync:
    container_name: adguardhome-sync
    image: ghcr.io/bakito/adguardhome-sync:latest
    command: run
    environment:
    ORIGIN_URL: ${ORIGIN_URL}
    ORIGIN_USERNAME: ${ORIGIN_USERNAME}
    ORIGIN_PASSWORD: ${ORIGIN_PASSWORD}
    REPLICA_URL: ${REPLICA_URL}
    REPLICA_USERNAME: ${REPLICA_USERNAME}
    REPLICA_PASSWORD: ${REPLICA_PASSWORD}
    CRON: "*/10 * * * *"
    RUN_ON_START: "true"
    networks:
    - proxy
    restart: unless-stopped
    keepalived:
    container_name: keepalived
    image: osixia/keepalived:latest
    network_mode: host
    cap_add:
    - NET_ADMIN
    - NET_BROADCAST
    - NET_RAW
    volumes:
    - ./keepalived/keepalived.conf:/etc/keepalived/keepalived.conf:ro
    restart: unless-stopped
    networks:
    proxy:
    name: proxy
    driver: bridge
    external: true

    Generate a scoped API token to automate DNS record management and SSL certificate validation.

    • Go to Dashboard > My Profile > API Tokens.
    • Create Token: Use the Edit zone DNS template.
    • Permissions: Select Zone | DNS | Edit.
    • Zone Resources: Select Include | Specific zone | your-domain.tld.
    Terminal window
    nano .env
    .env
    # pocket-id
    APP_URL=https://auth.{{BASE_SUB_DOMAIN_VAR}}
    TRUST_PROXY=true
    PUID=1000
    PGID=1000
    # Encryption key (choose one method):
    # Method 1: Direct key (simple but less secure)
    # Generate with: openssl rand -base64 32
    ENCRYPTION_KEY=
    # Method 2: File-based key (recommended)
    # Put the base64 key in a file and point to it here.
    # ENCRYPTION_KEY_FILE=/path/to/encryption_key
    # traefik
    CF_DNS_API_TOKEN=
    # traefik-forward-auth
    DEFAULT_PROVIDER=oidc
    PROVIDERS_OIDC_ISSUER_URL=https://auth.{{BASE_SUB_DOMAIN_VAR}}
    PROVIDERS_OIDC_CLIENT_ID=
    PROVIDERS_OIDC_CLIENT_SECRET=
    SECRET=# openssl rand -hex 16
    COOKIE_DOMAIN={{BASE_SUB_DOMAIN_VAR}}
    AUTH_HOST=auth.{{BASE_SUB_DOMAIN_VAR}}
    # adguardhome-sync
    ORIGIN_URL=http://{{SERVER_IP_VAR}}:8080
    ORIGIN_USERNAME=
    ORIGIN_PASSWORD=
    REPLICA_URL=http://{{CLIENT_IP_VAR}}:8080
    REPLICA_USERNAME=
    REPLICA_PASSWORD=
  2. Terminal window
    nano ./traefik/config/traefik.yml
    ./traefik/config/traefik.yml
    entryPoints:
    web:
    address: ":80"
    http:
    redirections:
    entryPoint:
    to: websecure
    scheme: https
    websecure:
    address: ":443"
    transport:
    respondingTimeouts:
    readTimeout: "30m"
    http:
    tls:
    certResolver: "cloudflare"
    providers:
    docker:
    exposedByDefault: false
    file:
    filename: "/etc/traefik/dynamic_config.yml"
    watch: true
    certificatesResolvers:
    cloudflare:
    acme:
    storage: "/letsencrypt/acme.json"
    # staging, remove for production
    caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
    dnsChallenge:
    provider: "cloudflare"
    resolvers:
    - "1.1.1.1:53"
    - "1.0.0.1:53"
    api:
    insecure: false
    dashboard: false
    log:
    level: "INFO"
    format: "common"
    Terminal window
    nano ./traefik/config/dynamic_config.yml
    ./traefik/config/dynamic_config.yml
    http:
    middlewares:
    pocket-id-auth:
    forwardAuth:
    address: "http://traefik-forward-auth:4181"
    authResponseHeaders:
    - "X-Forwarded-User"
    - "X-Forwarded-Email"
    trustForwardHeader: true
    hsts-header:
    headers:
    stsSeconds: 31536000
    stsIncludeSubdomains: true
    routers:
    router-pocket-id:
    rule: "Host(`auth.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-pocket-id
    entryPoints: ["websecure"]
    router-dashboard:
    rule: "Host(`traefik.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: api@internal
    entryPoints: ["websecure"]
    middlewares: ["pocket-id-auth"]
    router-adguard:
    rule: "Host(`adguard.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-adguard
    entryPoints: ["websecure"]
    middlewares: ["pocket-id-auth"]
    router-arcane:
    rule: "Host(`arcane.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-arcane
    entryPoints: ["websecure"]
    middlewares: ["hsts-header"]
    router-echo-link:
    rule: "Host(`echo.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-echo-link
    entryPoints: ["websecure"]
    router-immich:
    rule: "Host(`immich.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-immich
    entryPoints: ["websecure"]
    router-jellyfin:
    rule: "Host(`jf.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-jellyfin
    entryPoints: ["websecure"]
    router-linkding:
    rule: "Host(`link.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-linkding
    entryPoints: ["websecure"]
    router-dynacat:
    rule: "Host(`home.{{BASE_SUB_DOMAIN_VAR}}`)"
    service: service-dynacat
    entryPoints: ["websecure"]
    middlewares: ["hsts-header"]
    services:
    service-pocket-id:
    loadBalancer:
    servers:
    - url: "http://pocket-id:1411"
    service-adguard:
    loadBalancer:
    servers:
    - url: "{{SERVER_IP_VAR}}:8080"
    service-arcane:
    loadBalancer:
    servers:
    - url: "{{SERVER_IP_VAR}}:3552"
    service-echo-link:
    loadBalancer:
    servers:
    - url: "{{SERVER_IP_VAR}}:3002"
    service-immich:
    loadBalancer:
    servers:
    - url: "{{SERVER_IP_VAR}}:2283"
    service-jellyfin:
    loadBalancer:
    servers:
    - url: "{{SERVER_IP_VAR}}:8096"
    service-linkding:
    loadBalancer:
    servers:
    - url: "{{SERVER_IP_VAR}}:9090"
    service-dynacat:
    loadBalancer:
    servers:
    - url: "{{SERVER_IP_VAR}}:8282"
    Terminal window
    touch ./traefik/letsencrypt/acme.json && chmod 600 ./traefik/letsencrypt/acme.json
  3. Terminal window
    nano ./adguard/conf/AdGuardHome.yaml
    ./adguard/conf/AdGuardHome.yaml
    http:
    pprof:
    port: 6060
    enabled: false
    address: 0.0.0.0:8080
    session_ttl: 720h
    users: []
    auth_attempts: 5
    block_auth_min: 15
    http_proxy: ""
    language: ""
    theme: auto
    dns:
    bind_hosts:
    - 0.0.0.0
    port: 53
    anonymize_client_ip: false
    ratelimit: 20
    ratelimit_subnet_len_ipv4: 24
    ratelimit_subnet_len_ipv6: 56
    ratelimit_whitelist: []
    refuse_any: true
    upstream_dns:
    - https://dns.quad9.net/dns-query
    - https://dns.cloudflare.com/dns-query
    - https://dns.adguard-dns.com/dns-query
    upstream_dns_file: ""
    bootstrap_dns:
    - 9.9.9.10
    - 149.112.112.10
    - 2620:fe::10
    - 2620:fe::fe:10
    fallback_dns: []
    upstream_mode: load_balance
    fastest_timeout: 1s
    allowed_clients: []
    disallowed_clients: []
    blocked_hosts:
    - version.bind
    - id.server
    - hostname.bind
    trusted_proxies:
    - 127.0.0.0/8
    - ::1/128
    cache_enabled: true
    cache_size: 4194304
    cache_ttl_min: 0
    cache_ttl_max: 0
    cache_optimistic: false
    cache_optimistic_answer_ttl: 30s
    cache_optimistic_max_age: 12h
    bogus_nxdomain: []
    aaaa_disabled: false
    enable_dnssec: true
    edns_client_subnet:
    custom_ip: ""
    enabled: false
    use_custom: false
    max_goroutines: 300
    handle_ddr: true
    ipset: []
    ipset_file: ""
    bootstrap_prefer_ipv6: false
    upstream_timeout: 10s
    private_networks: []
    use_private_ptr_resolvers: true
    local_ptr_upstreams: []
    use_dns64: false
    dns64_prefixes: []
    serve_http3: false
    use_http3_upstreams: false
    serve_plain_dns: true
    hostsfile_enabled: true
    pending_requests:
    enabled: true
    tls:
    enabled: false
    server_name: ""
    force_https: false
    port_https: 443
    port_dns_over_tls: 853
    port_dns_over_quic: 853
    port_dnscrypt: 0
    dnscrypt_config_file: ""
    allow_unencrypted_doh: false
    certificate_chain: ""
    private_key: ""
    certificate_path: ""
    private_key_path: ""
    strict_sni_check: false
    querylog:
    dir_path: ""
    ignored: []
    interval: 2160h
    size_memory: 1000
    enabled: true
    file_enabled: true
    statistics:
    dir_path: ""
    ignored: []
    interval: 24h
    enabled: true
    filters:
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
    name: AdGuard DNS filter
    id: 1
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt
    name: AdAway Default Blocklist
    id: 2
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_51.txt
    name: HaGeZi's Pro++ Blocklist
    id: 1766894575
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_5.txt
    name: OISD Blocklist Small
    id: 1766894576
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_27.txt
    name: OISD Blocklist Big
    id: 1766894577
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_61.txt
    name: HaGeZi's Samsung Tracker Blocklist
    id: 1766894578
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_30.txt
    name: Phishing URL Blocklist (PhishTank and OpenPhish)
    id: 1769393156
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_56.txt
    name: HaGeZi's The World's Most Abused TLDs
    id: 1769393157
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_44.txt
    name: HaGeZi's Threat Intelligence Feeds
    id: 1769393158
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_55.txt
    name: HaGeZi's Badware Hoster Blocklist
    id: 1769393159
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_7.txt
    name: Perflyst and Dandelion Sprout's Smart-TV Blocklist
    id: 1773791901
    - enabled: true
    url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_52.txt
    name: HaGeZi's Encrypted DNS/VPN/TOR/Proxy Bypass
    id: 1773791902
    whitelist_filters: []
    user_rules:
    - "@@||tailscale.com^"
    - "@@||tailscale.io^"
    - "@@||log.tailscale.io^"
    - "||*.church^$denyallow=host.church|life.church|online.church,badfilter"
    - ""
    dhcp:
    enabled: false
    interface_name: ""
    local_domain_name: lan
    dhcpv4:
    gateway_ip: ""
    subnet_mask: ""
    range_start: ""
    range_end: ""
    lease_duration: 86400
    icmp_timeout_msec: 1000
    options: []
    dhcpv6:
    range_start: ""
    lease_duration: 86400
    ra_slaac_only: false
    ra_allow_slaac: false
    filtering:
    blocking_ipv4: ""
    blocking_ipv6: ""
    blocked_services:
    schedule:
    time_zone: UTC
    ids:
    - 4chan
    - 500px
    - 9gag
    - aliexpress
    - amino
    - betano
    - betfair
    - betway
    - bilibili
    - blaze
    - canais_globo
    - clubhouse
    - coolapk
    - crunchyroll
    - douban
    - kakaotalk
    - kik
    - kook
    - lazada
    - line
    - mail_ru
    - mercado_libre
    - odysee
    - ok
    - onlyfans
    - plenty_of_fish
    - shein
    - shopee
    - snapchat
    - temu
    - tiktok
    - tinder
    - wizz
    - xiaohongshu
    - zhihu
    protection_disabled_until: null
    safe_search:
    enabled: true
    bing: true
    duckduckgo: true
    ecosia: true
    google: true
    pixabay: true
    yandex: true
    youtube: true
    blocking_mode: default
    parental_block_host: family-block.dns.adguard.com
    safebrowsing_block_host: standard-block.dns.adguard.com
    rewrites:
    - domain: "*.{{BASE_SUB_DOMAIN_VAR}}"
    answer: "{{VIRTUAL_IP_VAR}}"
    enabled: true
    safe_fs_patterns:
    - /opt/adguardhome/work/userfilters/*
    safebrowsing_cache_size: 1048576
    safesearch_cache_size: 1048576
    parental_cache_size: 1048576
    cache_time: 30
    filters_update_interval: 24
    blocked_response_ttl: 10
    filtering_enabled: true
    rewrites_enabled: true
    parental_enabled: true
    safebrowsing_enabled: true
    protection_enabled: true
    clients:
    runtime_sources:
    whois: true
    arp: true
    rdns: true
    dhcp: true
    hosts: true
    persistent: []
    log:
    enabled: true
    file: ""
    max_backups: 0
    max_size: 100
    max_age: 3
    compress: false
    local_time: false
    verbose: false
    os:
    group: ""
    user: ""
    rlimit_nofile: 0
    schema_version: 32
  4. Terminal window
    nano ./keepalived/keepalived.conf
    ./keepalived/keepalived.conf
    global_defs {
    enable_script_security
    script_user root
    }
    vrrp_script check_adguard {
    script "/usr/bin/curl -fsS http://{{SERVER_IP_VAR}}:8080/control/status"
    interval 2
    weight 2
    }
    vrrp_instance VI_1 {
    state MASTER # MASTER | BACKUP
    interface enp3s0 # eth0 | enp3s0 | wlan0
    virtual_router_id 51
    priority 101 # MASTER: 101 | BACKUP: 100
    advert_int 1
    unicast_peer {
    # Add peers as necessary
    # 192.168.100.212 # peer 1
    # 192.168.100.213 # peer 2
    }
    authentication {
    auth_type PASS
    auth_pass password # Change max 8 characters
    }
    virtual_ipaddress {
    {{VIRTUAL_IP_VAR}}/24
    }
    track_script {
    check_adguard
    }
    }
  5. If another service on the host is already using port 53, disable the systemd-resolved stub listener before starting AdGuard Home.

    1. Terminal window
      sudo nano /etc/systemd/resolved.conf

      Set DNSStubListener=no

    2. Terminal window
      sudo systemctl restart systemd-resolved

    Update the Primary DNS, often located under LAN, DHCP or DNS Settings.

    • Primary DNS: {{SERVER_IP_VAR}} or {{VIRTUAL_IP_VAR}} (Your device’s Local LAN IP or virtual IP, not the Tailscale one).
    • Secondary DNS: 1.1.1.1 (Fail-safe so your internet still works if your device is turned off).

  6. Create the external Docker network used by Traefik, Pocket ID and the other proxy services.

    Terminal window
    docker network create proxy

    Start AdGuard Home first so its initial web setup can be completed before the remaining services depend on it.

    Terminal window
    docker compose up -d adguard

    Once the origin and replica AdGuard Home instances are configured and reachable, start the sync service.

    Terminal window
    docker compose up -d adguardhome-sync

    RUN_ON_START runs the first synchronization immediately, with subsequent synchronizations running every 10 minutes.

    Start Pocket ID and Traefik after the proxy network and AdGuard Home are ready.

    Terminal window
    docker compose up -d pocket-id traefik
    1. Open https://auth.{{BASE_SUB_DOMAIN_VAR}}/setup in your browser.
    2. Set up your admin account and register your primary Passkey.
    3. Go to Administration > OIDC Clients > Add OIDC Client:
      • Name: Traefik Forward Auth
      • Callback URL: https://auth.{{BASE_SUB_DOMAIN_VAR}}/_oauth
      • Check Public Client if secret is not enforced, or copy the Client ID and Client Secret.
    4. Paste those credentials into the PROVIDERS_OIDC_CLIENT_ID and PROVIDERS_OIDC_CLIENT_SECRET environment variables in .env.

    Start the authentication middleware after the Pocket ID OIDC client has been configured.

    Terminal window
    docker compose up -d traefik-forward-auth

    Start keepalived after the DNS services and virtual IP configuration are ready.

    Terminal window
    docker compose up -d keepalived