Configuring Consul DNS Forwarding in Alpine Linux

31 May 2019

DEPRECATED - This has a race condition!

Please see this post for an updated version which works!

Following on from the post the other day on setting up DNS forwarding to Consul with SystemD, I wanted also to show how to get Consul up and running under Alpine Linux, as it’s a little more awkward in some respects.

To start with, I am going to setup Consul as a service - I didn’t do this in the Ubuntu version, as there are plenty of useful articles about that already, but that is not the case with Alpine.

Run Consul

First, we need to get a version of Consul and install it into our system. This script downloads 1.5.1 from Hashicorp’s releases site, installs it to /usr/bin/consul, and creates a consul user and group to run the daemon with:


curl -sSL${CONSUL_VERSION}/consul_${CONSUL_VERSION} -o /tmp/

unzip /tmp/
sudo install consul /usr/bin/consul

sudo addgroup -S consul
sudo adduser -S -D -h /var/consul -s /sbin/nologin -G consul -g consul consul

Next, we need to create the directories for the configuration and data to live in, and copy the init script and configuration file to those directories:


sudo mkdir $consul_dir
sudo mkdir $data_dir
sudo chown consul:consul $data_dir

sudo mv /tmp/ /etc/init.d/consul
sudo chmod +x /etc/init.d/consul

sudo mv /tmp/consul.json $consul_dir/consul.json

The init script is pretty straight forward, but note that I am running the agent in this example in dev mode; don’t do this in production:


description="A tool for service discovery, monitoring and configuration"
description_checkconfig="Verify configuration file"

start_pre() {
    checkpath -f -m 0644 -o ${SVCNAME}:${SVCNAME} "$CONSUL_LOG_FILE"

depend() {
    need net
    after firewall

checkconfig() {
    consul validate $consul_dir

start() {
    checkconfig || return 1

    ebegin "Starting ${name}"
        start-stop-daemon --start --quiet \
            -m --pidfile /var/run/${name}.pid \
            --user ${daemon_user} --group ${daemon_group} \
            -b --stdout $CONSUL_LOG_FILE --stderr $CONSUL_LOG_FILE \
            -k 027 --exec ${daemon} -- agent -dev -config-dir=$consul_dir
    eend $?

stop() {
    ebegin "Stopping ${name}"
        start-stop-daemon --stop --quiet \
            --pidfile /var/run/${name}.pid \
            --exec ${daemon}
    eend $?

Finally, a basic config file to launch consul is as follows:

    "data_dir": "/srv/consul/data",
    "client_addr": ""

Now that all our scripts are in place, we can register Consul into the service manager, and start it:

sudo rc-update add consul
sudo rc-service consul start

You can check consul is up and running by using dig to get the address of the consul service itself:

dig @localhost -p 8600 consul.service.consul

Setup Local DNS with Unbound

Now that Consul is running, we need to configure a local DNS resolver to forward requests for the .consul domain to Consul. We will use Unbound as it works nicely on Alpine. It also has the wonderful feature of being able to send queries to a specific port, so no iptables rules needed this time!

The config file (/etc/unbound/unbound.conf) is all default values, with the exception of the last 5 lines, which let us forward DNS requests to a custom, and insecure, location:

#! /bin/bash

sudo apk add unbound

cat <<-EOF
    verbosity: 1
    root-hints: /etc/unbound/root.hints
    trust-anchor-file: "/usr/share/dnssec-root/trusted-key.key"
    do-not-query-localhost: no
    domain-insecure: "consul"
    name: "consul"
    stub-addr: [email protected]
) | sudo tee /etc/unbound/unbound.conf

sudo rc-update add unbound
sudo rc-service unbound start

We can validate this works again by using dig, but this time removing the port specification to hit 53 instead:

dig @localhost consul.service.consul

Configure DNS Resolution

Finally, we need to update /etc/resolv.conf so that other system tools such as ping and curl can resolve .consul addresses. This is a little more hassle on Alpine, as there are no head files we can push our nameserver entry into. Instead, we use dhclient which will let us prepend a custom nameserver (or multiple) when the interface is brought up, even when using DHCP:

#! /bin/bash

sudo apk add dhclient

cat <<-EOF
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
send host-name = gethostname();
request subnet-mask, broadcast-address, time-offset, routers,
        domain-name, domain-name-servers, domain-search, host-name,, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers,
        netbios-name-servers, netbios-scope, interface-mtu,
        rfc3442-classless-static-routes, ntp-servers;
prepend domain-name-servers;
) | sudo tee /etc/dhcp/dhclient.conf

sudo rm /etc/resolv.conf # hack due to it dhclient making an invalid `chown` call.
sudo rc-service networking restart

The only thing of interest here is the little hack: we delete the /etc/resolv.conf before restarting the networking service, as if you don’t do this, you get errors about “chmod invalid option resource=…”.

We can varify everything works in the same way we did on Ubuntu; curl to both a .consul and a public address:

$ curl -s -o /dev/null -w "%{http_code}\n" http://consul.service.consul:8500/ui/

$ curl -s -o /dev/null -w "%{http_code}\n"


This was a bit easier to get started with than the Ubuntu version as I knew what I was trying to accomplish this time - however making a good init.d script was a bit more hassle, and the error from chmod took some time to track down.

infrastructure, consul, alpine, dns

« Configuring Consul DNS Forwarding in Ubuntu 16.04 Feature Toggles: Branch by Abstraction »