docker-compose-external-dns

project-url project-url codacy-url

Docker Compose external DNS (docker-external-dns)

Repository: https://github.com/timk153/docker-external-dns

This project was inspired by:
https://github.com/kubernetes-sigs/external-dns
https://github.com/dntsk/extdns

It is broadly similar to extdns in functionality, but expands upon it.

This project was built using:
Nest framework TypeScript starter repository.

This project does the following:

  • Reads labels from containers sharing the same docker runtime.
    The labels contain DNS information.
  • Synchronises those records to CloudFlare
  • Runs at a regular interval (like a CRON job but interval is only programmable in seconds)
  • Supports DDNS (ipv4 only)
  • Supports multiple instances with different configurations
  • Writes identification comments to determine which records it controls
  • Supports the following record types only:
    • A
    • CNAME
    • MX
    • NS

IMPORTANT Only CloudFlare is supported.

User guide

The project provides a Docker Compose external DNS container specifically for CloudFlare. It supports DNS entries of types A, CNAME, NS, and MX. Configuration is managed through environment variables and labels applied to Docker containers. Detailed examples for various configurations and DNS record types are provided in the Examples section. For more details on DNS record types, refer to the DNS Entry Types section.

Check your supplied LOG_LEVEL.
Ensure it is one of: 'log', 'error', 'warn', 'debug', 'verbose', 'fatal'

If set to an invalid value the project will hand at start-up. I've tried to address this behavior unsuccessfully. For now awareness is the simplest solution.

Using the Docker Compose External DNS container is very straightforward. You need to declare an instance of it within your Docker Compose definition (docker-compose.y(a)ml) with the appropriate volume mount and environment variables set. Then, add some labels to your containers.

This file comprises the following sections:

The container is configured via environment variables. The following table describes the variable name, its default value (if any), and what it does. Detailed examples are available in the Examples section.

Variable Name Default Value Description
PROJECT_LABEL docker-compose-external-dns Detailed example available in the project Label and Instance ID section section.

Forms part of the label the project looks for on Docker containers to interpret as DNS entries. Also written as a comment to Cloudflare DNS entries managed by this instance of the project.
INSTANCE_ID 1 Detailed example available in the project Label and Instance ID section section.

Forms part of the label the project looks for on Docker containers to interpret as DNS entries. Also written as a comment to Cloudflare DNS entries managed by this instance of the project.
EXECUTION_FREQUENCY_SECONDS 60 How frequently the CRON job should execute to detect changes. Default is every 60 seconds. Undefined or empty uses the default. Minimum is every 1 second. There is no maximum. This must be an integer.
DDNS_EXECUTION_FREQUENCY_MINUTES 60 Determines how frequently the DDNS Service checks for a new public IP address. This setting only applies if you're using DDNS otherwise the service will not be started.
API_TOKEN You must supply either API_TOKEN or API_TOKEN_FILE but not both.

Your API token from Cloudflare. Must be granted Zone.Zone read and Zone.DNS edit.

IMPORTANT Use of this property is insecure as your API_TOKEN will be in plain text. It is recommended you use API_TOKEN_FILE. Use at your own risk.
API_TOKEN_FILE You must supply either API_TOKEN or API_TOKEN_FILE but not both.

Secure way to share your Cloudflare API Token with the project. Recommended approach for Docker Swarm. Compatible with Docker Compose (but less secure).

Read Docker Compose docs for more information: Docker Compose Secrets
LOG_LEVEL error The current logging level. The default is error, meaning only errors and fatal get logged.

Each level includes the levels above it from most specific to least specific. By way of example, verbose will output everything. debug will ignore verbose. log will ignore debug and verbose.

From most specific to least:
fatal
error
warn
log
debug
verbose

These log levels come from the NestJS project.

These two items combine to form the label which the project will look for on Docker Containers and write as the comment for that DNS entry into Cloudflare.

It is interpolated as follows: ${PROJECT_ID}:${INSTANCE_ID}

For example:

PROJECT_ID INSTANCE_ID EXAMPLE Use Case
docker-compose-external-dns 1 docker-compose-external-dns:1 The default
docker-compose-external-dns production docker-compose-external-dns:production Could be used to target a production Cloudflare subscription
docker-compose-external-dns production docker-compose-external-dns:non-production Targets the non-production Cloudflare subscription
dns.com.mydomain project dns.com.mydomain:project DNS entries for a specific subdomain of yours.

Please note, in a large deployment the project label and instance id will become cruitial to management.
For best practice, establish a naming convension and apply it consistently.

The values of the Docker Compose labels correspond to DNS entries. You can find descriptions of the various DNS records here: List of DNS Record Types.

Examples for all of these can be found in the Examples section below. For more details on the DNS record types, refer to the DNS Entry Types section.

The A record points a domain name to an IP address.


name = example.com
server = 8.8.8.8

Lookup of example.com returns 8.8.8.8

The properties required for this entry are as follows:

property value description
type A The type of the record. In this case it should be A
name <your domain name> This is the domain you want this A record to resolve for. For example: example-domain.com
address <your server's address (v4 or 6)> OR "DDNS" The address you want your domain name to resolve to.
Or the string literal "DDNS" which instructs the project to compute your current public ipv4 address and use it for this record.
proxy true or false True uses Cloudflare's proxy to hide your address. False causes Cloudflare to act as a normal DNS server.

Documentation: Proxied DNS Records

The CNAME record aliases the name to another A or CNAME record. Causing the name when queried to resolve to the A record it (eventually) resolves to.


type = A
name = example.com
server = 8.8.8.8

type = CNAME
name = subdomain.example.com
target = example.com

type = CNAME
name = lower.subdomain.example.com
target = subdomain.example.com

Lookup of example.com returns 8.8.8.8
Lookup of subdomain.example.com returns 8.8.8.8
Lookup of lower.subdomain.example.com returns 8.8.8.8

The properties required for this entry are as follows:

property value description
type CNAME The type of the record. In this case it should be CNAME
name <your alias> This is the alias you want this CNAME record to resolve for. For example: subdomain.example-domain.com
target <your full A record or CNAME record> The full name of the relevant A or CNAME record this should resolve to. For example, use 'example-domain.com' to point at the A record above.
proxy true or false True uses Cloudflare's proxy to hide your address. False causes Cloudflare to act as a normal DNS server.

Documentation: Proxied DNS Records

Please note, only one level of subdomain can be proxied. If it's two subdomains deep (e.g. test.subdomain.example.com) it cannot be proxied unless you have a premium subscription.

The MX record declares that a mail server handles mail for your domain or subdomain. The name is the domain or subdomain it handles mail for. The server points to an A or CNAME which resolves to your mail server. It's common to make a CNAME record for the mail server for this entry to point to.


type = A
name = example.com
server = 8.8.8.8

type = CNAME
name = mx1.example.com
target = example.com

type = MX
name = example.com
target = mx1.example.com

Lookup of example.com returns 8.8.8.8
Lookup of mx1.example.com returns 8.8.8.8
Lookup of mail server for example.com returns 8.8.8.8

The properties required for this entry are as follows:

property value description
type MX The type of the record. In this case it should be MX
name <your domain> This is the domain you want this mail server to handle mail for. For example: example-domain.com
server <full name of the mail server> The full name of the relevant A or CNAME entry this should resolve to. For example 'mx1.example-domain.com' (assuming you've made a CNAME or A record resolving mx1).
priority 0 to 65535 The priority of this mail server, allowing you to have more than one mail server for a domain. Must be an integer between the stated values.

The NS record points a domain or subdomain name to an A or CNAME record. DNS queries that match that domain or subdomain are forwarded to the server. This is typically used to point traffic on a subdomain to another name server. Such as one only accessible within a private network.


type = A
name = lan.example.com
server = 192.168.0.1

type = NS
name = example.com
server = lan.example.com

Lookup of example.com returns lan.example.com
Lookup of lan.example.com returns 192.168.0.1

The properties required for this entry are as follows:

property value description
type NS The type of the record. In this case it should be NS
name <your domain name> This is the domain you want to forward queries for. For example: example-domain.com
server <your server name> The full name of the relevant A or CNAME entry this should resolve to. For example 'lan.example-domain.com'

There are four types of image tag associated with this project:

tag example description
latest timk153/docker-external-dns:latest the latest release of the most recent major version
<major version number>-latest timk153/docker-external-dns:1-latest the latest release of that major version. In the example it's the latest release of version 1.
semantic version number timk153/docker-external-dns:1.4.2 a specific release. In the example it's release 1.4.2
semantic version with additional identifier timk153/docker-external-dns:1.4.2-alpha a alpha, beta or development build. In the example it's an alpha release of version 1.4.2.

All available tags can be found in the docker hub public registry.

Below are a series of example configurations for the following usecases. Please note, all examples use the image tagged with latest.

This example demonstrates the most basic setup of the Docker Compose External DNS container with default values. It shows how to use the API_TOKEN environment variable and configure a single DNS entry.

IMPORTANT API_TOKEN is insecure, API_TOKEN_FILE is recommended

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false }]'

Explanation: This setup includes the docker-compose-external-dns service configured with the API_TOKEN environment variable. It will use the token to authenticate with Cloudflare. The other-service has a label that specifies a DNS A record for my-domain.com pointing to 8.8.8.8 with no proxy.

This configuration demonstrates the preferred method of passing the API token securely using Docker secrets. This is more secure than using API_TOKEN.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN_FILE=/run/secrets/CLOUDFLARE_API_TOKEN
secrets:
- CLOUDFLARE_API_TOKEN
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false }]'

secrets:
CLOUDFLARE_API_TOKEN:
environment: 'CLOUDFLARE_API_TOKEN'

Explanation: This setup uses Docker secrets to securely manage the Cloudflare API token. The API_TOKEN_FILE environment variable points to the secret file, which contains the API token. This approach is recommended for better security, especially in production environments.

This example shows how to customize the label, instance ID, execution frequency, and log level settings.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- PROJECT_LABEL=dns.com.example
- INSTANCE_ID=project-subdomain
- EXECUTION_FREQUENCY_SECONDS=120
- LOG_LEVEL=info
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'dns.com.example:project-subdomain=[{ "type": "CNAME", "name": "project.example.com", "target": "example.com", "proxy": true }]'

Explanation: In this configuration:

PROJECT_LABEL is set to dns.com.example, which is used as part of the label on Docker containers.
INSTANCE_ID is set to project-subdomain to differentiate this instance.
EXECUTION_FREQUENCY_SECONDS is set to 120, meaning the DNS updates will occur every 2 minutes.
LOG_LEVEL is set to info to log informational messages as well as warnings and errors.
The other-service has a CNAME record pointing project.example.com to example.com with proxy enabled.

This example illustrates managing DNS entries for multiple domains using a single docker-compose-external-dns service. Please note, your API_TOKEN(_FILE) will require permissions for both domains.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[
{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false },
{ "type": "A", "name": "my-other-domain.org", "address": "8.8.8.8", "proxy": true }]'

Explanation: This setup shows how to handle DNS records for two different domains (my-domain.com and my-other-domain.org) with one instance of docker-compose-external-dns. Each domain has its own A record configuration. The my-other-domain.org entry uses Cloudflare's proxy.

These configurations demonstrates how to manage DNS records for different domains using separate docker-compose-external-dns services.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns-1:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<api token for my-domain.com here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

docker-compose-external-dns-2:
image: 'timk153/docker-external-dns:latest'
environment:
- INSTANCE_ID=2
- API_TOKEN=<api token for my-other-domain.org here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false }]'
- 'docker-compose-external-dns:2=[{ "type": "A", "name": "my-other-domain.org", "address": "8.8.8.8", "proxy": true }]'

Explanation: This setup uses two separate docker-compose-external-dns services to manage DNS entries for my-domain.com and my-other-domain.org. Each service is configured with its own API token and instance ID. This allows for independent management of DNS entries for each domain.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns-1:
image: 'timk153/docker-external-dns:latest'
environment:
- PROJECT_LABEL=dns.com.my-domain
- API_TOKEN=<api token for my-domain.com here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

docker-compose-external-dns-2:
image: 'timk153/docker-external-dns:latest'
environment:
- PROJECT_LABEL=dns.org.my-other-domain
- API_TOKEN=<api token for my-other-domain.org here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'dns.com.my-domain:1=[{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false }]'
- 'dns.org.my-other-domain:1=[{ "type": "A", "name": "my-other-domain.org", "address": "8.8.8.8", "proxy": true }]'

Explanation: This setup uses two separate docker-compose-external-dns services to manage DNS entries for my-domain.com and my-other-domain.org. Each service is configured with its own API token and project label. This allows for independent management of DNS entries for each domain.

The final set of examples demonstrates different types of DNS records (A, CNAME, NS, MX) and how to configure them using Docker Compose labels. Please note, these labels may live on one or more services spead across one or more docker-compose files running on Docker. They are all on "other-service" in this instance for simplicity sake.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false }]'

Explanation: This example configures an A record for my-domain.com pointing to 8.8.8.8 without using Cloudflare's proxy.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[{ "type": "A", "name": "my-domain.com", "address": "DDNS", "proxy": false }]'

Explanation: This example configures an A record for my-domain.com. The project will start the DDNS Service when this record is processed. The service will fetch your current public ipv4 address and use it for this record. The DDNS Service will check at regular intervals for a new ipv4 address. If one is detected then this record will be updated to the new value when the next DNS synchronisation interval is reached.

Settings to control interval are explained in the configuration section.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[
{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false },
{ "type": "CNAME", "name": "sub.my-domain.com", "target": "my-domain.com", "proxy": false }]'

Explanation: This setup includes a CNAME record that aliases sub.my-domain.com to my-domain.com, following the A record configuration for my-domain.com.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[
{ "type": "A", "name": "lan.my-domain.com", "address": "192.168.0.1", "proxy": false },
{ "type": "CNAME", "name": "ns1.lan.my-domain.com", "target": "lan.my-domain.com", "proxy": false },
{ "type": "NS", "name": "lan.my-domain.com", "server": "ns1.lan.my-domain.com" }]'

Explanation: This configuration includes an NS record specifying ns1.lan.my-domain.com as the nameserver for lan.my-domain.com, alongside an A record for lan.my-domain.com.

IMPORTANT example uses insecure option "API_TOKEN" for simplicity.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[
{ "type": "A", "name": "my-domain.com", "address": "8.8.8.8", "proxy": false },
{ "type": "CNAME", "name": "mx1.my-domain.com", "target": "my-domain.com", "proxy": false },
{ "type": "MX", "name": "my-domain.com", "server": "mx1.my-domain.com", "priority": 0 }]'

Explanation: This example sets up an MX record for my-domain.com that points to mx1.my-domain.com with a priority of 0. This is used to specify the mail server for the domain.

services:
docker-compose-external-dns:
image: 'timk153/docker-external-dns:latest'
environment:
- API_TOKEN=<your api token here>
volumes:
# Used to read labels from containers - readonly
- '/var/run/docker.sock:/var/run/docker.sock:ro'

other-service:
image: 'busybox:latest'
command: 'sleep 3600'
labels:
- 'docker-compose-external-dns:1=[
{ "type": "A", "name": "my-domain.com", "address": "DDNS", "proxy": false },
{ "type": "CNAME", "name": "mx1.my-domain.com", "target": "my-domain.com", "proxy": false },
{ "type": "MX", "name": "my-domain.com", "server": "mx1.my-domain.com", "priority": 0 },
{ "type": "CNAME", "name": "subdomain.my-domain.com", "target": "my-domain.com", "proxy": false },
{ "type": "A", "name": "lan.my-domain.com", "address": "192.168.0.1", "proxy": false },
{ "type": "CNAME", "name": "ns1.lan.my-domain.com", "target": "lan.my-domain.com", "proxy": false },
{ "type": "NS", "name": "lan.my-domain.com", "server": "ns1.lan.my-domain.com" }]'