CoreDNS - a flexible DNS server written in Go https://coredns.io
  • Go 54.2%
  • Dockerfile 45.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Elisamuel Resto f80122db2d
All checks were successful
CoreDNS / ⌚ Build and Deploy (push) Successful in 14m34s
docs(ci): say what the nightly rebuild is actually for
It was annotated as picking up hardened base image updates, but the DHI tier in
use gets no new date-stamped tags, so the base is effectively fixed at the pin
in the Dockerfile.

What the rebuild does refresh is the Go toolchain: setup-go tracks stable, so a
Go patch release is compiled in without waiting for the next commit. Worth
recording, or the cron reads as pointless and gets removed.
2026-08-27 14:08:38 -05:00
.forgejo/workflows docs(ci): say what the nightly rebuild is actually for 2026-08-27 14:08:38 -05:00
cmd/coredns feat(cmd): CoreDNS distribution with the nextdns plugin compiled in 2026-08-27 02:37:51 -05:00
deploy/kubernetes docs(k8s): deployment manifests that work without an unsafe sysctl 2026-08-27 02:38:19 -05:00
.dockerignore ci: drop goreleaser, build the image directly 2026-08-27 04:05:34 -05:00
.gitignore chore: ignore the built binary 2026-08-27 02:38:38 -05:00
compose.yaml feat(docker): hardened rootless container image 2026-08-27 02:38:07 -05:00
Corefile.example feat(docker): hardened rootless container image 2026-08-27 02:38:07 -05:00
Dockerfile ci: drop goreleaser, build the image directly 2026-08-27 04:05:34 -05:00
go.mod build(deps): update to plugin v0.1.1, miekg/dns v1.1.73, caddy v1.1.4 2026-08-27 03:14:44 -05:00
go.sum build(deps): update to plugin v0.1.1, miekg/dns v1.1.73, caddy v1.1.4 2026-08-27 03:14:44 -05:00
README.md ci: drop goreleaser, build the image directly 2026-08-27 04:05:34 -05:00

coredns

This homelab's CoreDNS distribution: upstream CoreDNS plus the external plugins it needs, built into one hardened container image.

CoreDNS has no runtime plugin loading, so carrying an external plugin means building your own binary. This repository is that build — the plugins themselves live in their own repositories and are consumed as ordinary Go modules.

Plugin Source Corefile position
nextdns user00265/coredns-nextdns after forward

Everything in upstream's plugin.cfg comes along unchanged — cache, forward, log, errors, health, ready, prometheus, hosts, template, view, metadata and the rest. A plugin being compiled in does not make it active; the Corefile decides that.

Image

scm.nullnetwork.cc/homelab/coredns:latest

linux/amd64 and linux/arm64. Built on the Docker Hardened Images static base: distroless, nonroot 65532, no shell and no package manager. The binary is CGO_ENABLED=0 and stripped.

No setcap, no capabilities, no root. Binding 53, 443 or 853 as an unprivileged user is left to net.ipv4.ip_unprivileged_port_start=0, which compose.yaml sets.

Ports

Port Needs
53/udp, 53/tcp plain DNS
443/tcp DoH (https://), or gRPC (grpc://) TLS material
443/udp DoH over HTTP/3 TLS material
853/tcp DoT (tls://) TLS material
853/udp DoQ (quic://) — RFC 9250 shares the DoT port TLS material
8080/tcp health health in the Corefile
8181/tcp ready ready in the Corefile
9153/tcp prometheus prometheus :9153 — the plugin defaults to localhost

pprof (localhost:6053) is deliberately not exposed: it serves heap and goroutine dumps. Map it yourself if you need it.

CoreDNS does not do ACME. The tls plugin reads certificate files you provide.

Running

Docker — see compose.yaml. Mount a single Corefile read-only; the image bakes none in, and CoreDNS keeps no state worth a volume.

cp Corefile.example Corefile   # then edit in your profile ID
docker compose up -d

Kubernetes — see deploy/kubernetes/coredns.yaml. It listens high in the container and lets the Service map 53 onto it, because net.ipv4.ip_unprivileged_port_start is an unsafe sysctl that kubelet rejects unless started with --allowed-unsafe-sysctls. The manifest documents the direct-bind alternative if you control kubelet.

Note externalTrafficPolicy: Local in the Service: the nextdns plugin attributes queries by source address, and the default policy SNATs it to a node address, which would collapse every device into one.

Building

go build -o coredns ./cmd/coredns

cmd/coredns/main.go imports core/plugin for the stock set, imports each external plugin, and splices its directive into dnsserver.Directives at the right position — the equivalent of adding a plugin.cfg line, without needing a CoreDNS checkout or its generator.

To add a plugin: import it in main.go and add an entry to external naming the stock directive it should follow. The tests check that every entry lands in the right place.

Releases

There are none, deliberately. What an image contains is decided by go.mod — which CoreDNS, which plugins — so a version of its own here would be a third number with no independent meaning. Every push to master publishes, and a nightly rebuild picks up hardened base image updates.

Image tags describe the contents instead:

tag
latest moves with master
1.14.7 the CoreDNS carried; moves as plugins and the base image update
1.14.7-abc1234 immutable and exact — pin or roll back to this

The CoreDNS version is read out of go.mod by the workflow rather than written by hand, so it cannot drift.

The Dockerfile does the cross-compiling itself: the build stage is pinned to $BUILDPLATFORM and Go targets $TARGETARCH from there, so a multi-arch build never runs anything under emulation. See .forgejo/workflows/.