AdGuard Home
Deploy AdGuard Home on Kubernetes — a network-wide DNS-level ad and tracker blocker. Supports DNS over HTTPS, DNS over TLS, custom rewrite rules, client-level settings, and optional multi-instance synchronization.
- Wizard mode (
config.adGuardHome: {}): AdGuard Home starts on port 3000 for the initial setup wizard and remains on that listener after setup, keeping the Service target stable. - Pre-configured mode (
config.adGuardHomepopulated): Setup wizard is skipped entirely. The UI starts directly on port 80, ready for production use.
Choose pre-configured mode for GitOps or reproducible deployments. Wizard mode is suitable for first-time manual setup.
Key Features
- Two deployment modes — interactive wizard or full pre-configured via values
- Dual PVC — separate
conf(config + TLS certs) andwork(query log + stats + filters) volumes - Dedicated DNS service — separate LoadBalancer for UDP/TCP port 53 with optional named ports such as DNS-over-TLS
- adguardhome-sync — multi-instance config synchronization (primary → replicas)
- Full-volume backup —
tararchives both PVCs to S3 (no database involved)
Installation
HTTPS repository:
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install adguard-home helmforge/adguard-home -f values.yaml
OCI registry:
helm install adguard-home oci://ghcr.io/helmforgedev/helm/adguard-home -f values.yaml
Deployment Examples
# values.yaml — AdGuard Home in wizard mode (default)
# Web UI starts on port 3000 for first-time setup
# The chart keeps the listener on port 3000 after setup
service:
dns:
type: LoadBalancer
loadBalancerIP: '192.168.1.53' # static IP for DNS (optional, MetalLB/cloud LB)
extraPorts:
- name: dns-tls
port: 853
targetPort: 853
protocol: TCP
persistence:
conf:
enabled: true
size: 256Mi
work:
enabled: true
size: 5Gi # query log + filter lists + statistics
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: adguard.example.com
paths:
- path: /
pathType: Prefix# Access wizard for first-time setup:
kubectl port-forward svc/<release>-adguard-home-web 3000:80
# Visit http://localhost:3000# values.yaml — AdGuard Home pre-configured (wizard skipped, UI on port 80)
config:
adGuardHome:
http:
address: 0.0.0.0:80
session_ttl: 720h
users:
- name: admin
# Generate: htpasswd -bnBC 10 "" 'mypassword' | cut -d: -f2
password: '$2y$10$...'
dns:
bind_hosts:
- 0.0.0.0
port: 53
upstream_dns:
- 'https://dns.cloudflare.com/dns-query'
- 'https://dns.google/dns-query'
bootstrap_dns:
- 1.1.1.1
- 8.8.8.8
protection_enabled: true
filtering_enabled: true
cache_size: 4194304
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
querylog:
enabled: true
interval: 720h
statistics:
enabled: true
interval: 168h
schema_version: 29
service:
dns:
type: LoadBalancer
loadBalancerIP: '192.168.1.53'
persistence:
conf:
enabled: true
size: 256Mi
work:
enabled: true
size: 5Gi
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: adguard.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: adguard-home-tls
hosts:
- adguard.example.com# values.yaml — Primary instance with adguardhome-sync to replica
# Deploy a second instance as the replica (separate helm release)
config:
adGuardHome:
http:
address: 0.0.0.0:80
users:
- name: admin
password: '$2y$10$...'
schema_version: 29
sync:
enabled: true
origin:
url: 'http://adguard-home.adguard.svc.cluster.local:80'
username: admin
password: 'admin-password' # use existingSecret in production
replicas:
- url: 'http://adguard-home-replica.adguard.svc.cluster.local:80'
username: admin
password: 'admin-password'
cron: '*/10 * * * *' # sync every 10 minutes; empty = continuous daemon
runOnStart: true
features:
dns:
serverConfig: true
accessLists: true
rewrites: true
general:
settings: true
protection: true
clients: true
filters: true
service:
dns:
type: LoadBalancer
loadBalancerIP: '192.168.1.53'# values.yaml — AdGuard Home with daily S3 backup
# Backup archives both conf and work PVCs (tar — no database)
config:
adGuardHome:
http:
address: 0.0.0.0:80
users:
- name: admin
password: '$2y$10$...'
schema_version: 29
service:
dns:
type: LoadBalancer
loadBalancerIP: '192.168.1.53'
persistence:
conf:
enabled: true
size: 256Mi
work:
enabled: true
size: 5Gi
backup:
enabled: true
schedule: '0 2 * * *'
s3:
endpoint: https://s3.amazonaws.com
bucket: adguard-backups
prefix: adguard-home
existingSecret: adguard-s3-credentials # keys: access-key, secret-key
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: adguard.example.com
paths:
- path: /
pathType: PrefixConfiguration Reference
Image
| Parameter | Type | Default | Description |
|---|---|---|---|
image.repository |
string | docker.io/adguard/adguardhome |
AdGuard Home image. |
image.tag |
string | "v0.107.79" |
Image tag. |
Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
config.adGuardHome |
object | {} |
Pre-seed config rendered as AdGuardHome.yaml. Empty = wizard mode. |
config.existingSecret |
string | "" |
Existing secret with AdGuardHome.yaml content (key: AdGuardHome.yaml). |
Application Listeners
| Parameter | Type | Default | Description |
|---|---|---|---|
containerPorts.setup |
integer | 3000 |
Wizard listener passed to AdGuard Home through --web-addr. |
containerPorts.http |
integer | 80 |
Configured admin UI listener passed through --web-addr. |
containerPorts.dns |
integer | 53 |
DNS listener written into chart-managed AdGuardHome.yaml. |
Application listeners and public Service ports are independent. Services target named Pod ports,
so service.web.port and service.dns.port can differ from the process listeners. When using
config.existingSecret or an already initialized configuration PVC, ensure its dns.port matches
containerPorts.dns; Helm cannot inspect or rewrite that persisted configuration.
In wizard mode, select containerPorts.dns as the DNS listener before completing
setup. The wizard otherwise proposes port 53, which will not match a custom
Service target.
AdGuard Home requires bcrypt-hashed passwords in the configuration file. Generate with:
htpasswd -bnBC 10 "" 'mypassword' | cut -d: -f2Use the output (starting with $2y$10$...) as the password value.
Persistence — Dual PVC
| Parameter | Type | Default | Description |
|---|---|---|---|
persistence.conf.enabled |
boolean | true |
Enable PVC for /opt/adguardhome/conf (config + TLS certs). |
persistence.conf.size |
string | 256Mi |
Conf PVC size. |
persistence.conf.storageClass |
string | "" |
StorageClass for conf PVC. |
persistence.conf.existingClaim |
string | "" |
Use an existing PVC for conf. |
persistence.work.enabled |
boolean | true |
Enable PVC for /opt/adguardhome/work (query log + stats + filter lists). |
persistence.work.size |
string | 2Gi |
Work PVC size. Increase for longer query log retention. |
persistence.work.storageClass |
string | "" |
StorageClass for work PVC. |
persistence.work.existingClaim |
string | "" |
Use an existing PVC for work. |
Init Permissions
| Parameter | Type | Default | Description |
|---|---|---|---|
initPermissions.enabled |
boolean | true |
Normalize /opt/adguardhome/conf and /opt/adguardhome/work before start. |
initPermissions.image.repository |
string | docker.io/library/busybox |
Init container image repository. |
initPermissions.image.tag |
string | "1.37" |
Init container image tag. |
initPermissions.securityContext.capabilities.add |
array | [CHOWN, DAC_OVERRIDE, FOWNER] |
Baseline-compatible capabilities for PVC ownership, recursive traversal, and directory mode changes. |
initPermissions.resources |
object | {} |
Resource requests and limits for the init container. |
When securityContext.runAsUser or podSecurityContext.runAsUser is configured for a non-root runtime, pre-seed AdGuardHome.yaml with
config.adGuardHome or config.existingSecret. The interactive setup wizard requires administrator privileges on first launch;
pre-seeded mode runs with non-root UID and podSecurityContext.fsGroup.
Kubernetes API token automount is disabled by default for the server, sync, and backup workloads.
The optional NetworkPolicy admits only the active web and DNS listener ports. An empty
networkPolicy.ingressFrom preserves access from all sources, including external DNS clients;
set explicit peers to restrict sources. Egress remains unrestricted unless extraEgress is non-empty.
| Parameter | Type | Default | Description |
|---|---|---|---|
serviceAccount.automountServiceAccountToken |
boolean | false |
Mount a Kubernetes API token into chart workloads. |
networkPolicy.enabled |
boolean | false |
Render the server NetworkPolicy. |
networkPolicy.ingressFrom |
array | [] |
Optional peers allowed to reach the declared listeners. |
networkPolicy.extraIngress |
array | [] |
Additional complete ingress rules. |
networkPolicy.extraEgress |
array | [] |
Complete egress rules; non-empty enables egress isolation. |
Services
| Parameter | Type | Default | Description |
|---|---|---|---|
service.web.type |
string | ClusterIP |
Web UI service type. |
service.web.port |
integer | 80 |
Web UI port. |
service.dns.type |
string | LoadBalancer |
DNS service type. Use NodePort for bare-metal without MetalLB. |
service.dns.port |
integer | 53 |
Built-in DNS service port, exposed over UDP and TCP. |
service.dns.extraPorts |
array | [] |
Additional explicit name, port, targetPort, and protocol entries, such as DoT on TCP 853. |
service.dns.loadBalancerIP |
string | "" |
Static IP for the DNS LoadBalancer. |
Ingress
| Parameter | Type | Default | Description |
|---|---|---|---|
ingress.enabled |
boolean | false |
Enable Ingress for the web UI. |
ingress.ingressClassName |
string | traefik |
Ingress class name. |
ingress.hosts |
array | [] |
Host and path rules. |
ingress.tls |
array | [] |
TLS configuration. |
Sync (adguardhome-sync)
| Parameter | Type | Default | Description |
|---|---|---|---|
sync.enabled |
boolean | false |
Deploy the adguardhome-sync sidecar. |
sync.origin.url |
string | "" |
URL of the origin AdGuard Home instance. |
sync.origin.username |
string | "" |
Admin username on the origin. |
sync.origin.password |
string | "" |
Admin password on the origin. Use existingSecret. |
sync.replicas |
array | [] |
List of replica instances (url, username, password). |
sync.cron |
string | */10 * * * * |
Sync schedule. Empty string = continuous daemon mode. |
sync.runOnStart |
boolean | true |
Run sync immediately on pod startup. |
sync.existingSecret |
string | "" |
Existing secret with sync credentials. |
sync.features.dns.serverConfig |
boolean | true |
Sync DNS server configuration. |
sync.features.general.filters |
boolean | true |
Sync filter lists. |
sync.features.general.clients |
boolean | true |
Sync client settings. |
Backup
Backup archives both PVCs (conf + work) as tar archives. No database is involved.
| Parameter | Type | Default | Description |
|---|---|---|---|
backup.enabled |
boolean | false |
Enable scheduled S3 backup CronJob. |
backup.schedule |
string | "0 2 * * *" |
Cron schedule. |
backup.archivePrefix |
string | adguard-home |
Prefix for backup archive filenames. |
backup.s3.endpoint |
string | "" |
S3-compatible endpoint URL. |
backup.s3.bucket |
string | "" |
Target bucket name. |
backup.s3.existingSecret |
string | "" |
Existing secret with S3 credentials. |
extraManifests |
array | [] |
Extra Kubernetes manifests. |