├── Dockerfile ├── LICENSE ├── README.md ├── bin └── install └── docker-files ├── etc └── dnsmasq.tmpl └── usr └── local └── bin ├── dnsmasq-reload └── entrypoint /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | LABEL maintainer="Jérémy Derussé " 3 | 4 | RUN apk --no-cache add \ 5 | dnsmasq \ 6 | openssl 7 | 8 | ENV DOCKER_GEN_VERSION 0.7.4 9 | ENV DOCKER_HOST unix:///var/run/docker.sock 10 | 11 | RUN wget -qO- https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-alpine-linux-amd64-$DOCKER_GEN_VERSION.tar.gz | tar xvz -C /usr/local/bin 12 | COPY docker-files/. / 13 | 14 | VOLUME /var/run 15 | EXPOSE 53/udp 16 | ENV GATEWAY=172.17.42.1 \ 17 | RESOLV_PATH= 18 | 19 | ENTRYPOINT ["entrypoint"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Jérémy Derussé 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker DNS-gen 2 | 3 | dns-gen sets up a container running Dnsmasq and [docker-gen]. 4 | docker-gen generates a configuration for Dnsmasq and reloads it when containers are 5 | started and stopped. 6 | 7 | By default it will provide thoses domain: 8 | - `container_name.docker` 9 | - `container_name.network_name.docker` 10 | - `docker-composer_service.docker-composer_project.docker` 11 | - `docker-composer_service.docker-composer_project.network_name.docker` 12 | 13 | **easy install:** 14 | 15 | sudo sh -c "$(curl -fsSL https://raw.githubusercontent.com/jderusse/docker-dns-gen/master/bin/install)" 16 | 17 | ## How it works 18 | 19 | The container `dns-gen` expose a standard dnsmasq service. It returns ip of 20 | known container and fallback to host's resolv.conf for other domains. 21 | 22 | ## Requirement 23 | 24 | In order to ease the configuration of your host. We recommand to install 25 | `resolvconf` 26 | 27 | apt install resolvconf 28 | 29 | The container have to listen on port `53` on `docker0` interface. You should 30 | assert that nothing else is listening to that port. 31 | 32 | sudo netstat -ntlp --udp|grep ":53 " 33 | 34 | If some service are listening to the same port, you should changes your 35 | setting to exclude the `docker0` interface. 36 | For instance, in dnsmasq use : 37 | 38 | except-interface=docker0 39 | 40 | ## Q/A 41 | 42 | **Why mounting the entier host in the container (`-v /:/host`)** 43 | 44 | The dnsmasq embeded inside the container is configured to fallback to the 45 | default `resolv.conf`. But, given the host is configured to use the container 46 | to resolve DNS, and to avoid infinite loop, the container uses the hosts's 47 | `/etc/resolv.conf` without it own IP. 48 | 49 | Mounting the `-v /etc/resolv.conf:/etc/resolv.conf` file is not possible as 50 | this file is often overriden by the host (changing network, connecting to 51 | wifi, connecting to a VPN) and docker wouldn't update the mounted file. And, 52 | in most of the cases, this file is a symlink to another location, and the the 53 | target should be accessible by the container too. 54 | 55 | For instances, when using resolvconf on Debian 9: 56 | 57 | /etc/resolv.conf -> /etc/resolvconf/run/resolv.conf 58 | /etc/resolvconf/run -> /run/resolvconf 59 | 60 | **Why not using the container's `/etc/resolv.conf` file?** 61 | 62 | First of all, because the user could have configured the `/etc/docker/daemon.json` 63 | to use a specific `name server` inside the container, whereas we are trying to 64 | configure the host's DNS resolution. 65 | 66 | Moreover, from my tests, the file is not updated when the host changes it `name 67 | servers`. 68 | 69 | **Why using the host network** 70 | 71 | Some distributions (like ubuntu) use a local dnsmasq instance to resolv DNS 72 | This instance, runing on the host local network, should be accessible by the 73 | container. Using the `host` network is the easiest way to be as close as 74 | possible to the host. 75 | 76 | ## Previous version of docker-gen 77 | 78 | If you already install and configured a previous version of dns-gen you should 79 | uninstall it. 80 | 81 | - uninstall dnsmasq 82 | - remove previous container `docker rm -fv dns-gen` 83 | - remove custom configuration in `/etc/docker/daemon.json` 84 | - remove /etc/NetworkManager/dnsmasq.d/01_docker 85 | - restart the service NetworkManager (and in some case the machine) 86 | 87 | ## Manual Install 88 | 89 | Just run the `all in one` script `./bin/install` 90 | 91 | If you want to deep inside, here are the manuall steps to permform the same 92 | 93 | First, you have to know the IP of your `docker0` interface. It may be 94 | `172.17.42.1` but it could be something else. To known your IP, run the 95 | following command: 96 | 97 | GATEWAY=$(ip -4 addr show docker0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}') 98 | echo ${GATEWAY} 99 | 100 | Now, you can start the `dns-gen` container: 101 | 102 | docker run -d --name dns-gen \ 103 | --restart always \ 104 | --net host \ 105 | -e GATEWAY=$GATEWAY \ 106 | --log-opt "max-size=10m" \ 107 | --volume /:/host \ 108 | --volume /var/run/docker.sock:/var/run/docker.sock \ 109 | jderusse/dns-gen:2 110 | 111 | You can test the container 112 | 113 | docker run --name test nginx:alpine 114 | dig test.docker @${GATEWAY} 115 | 116 | Once OK, you can finally update your local resolver 117 | 118 | echo "nameserver ${GATEWAY}" | sudo tee --append /etc/resolvconf/resolv.conf.d/head 119 | resolvconf -u 120 | 121 | **beware**! When your host will restart, it may change the IP address of 122 | the `docker0` interface. You may have to run the command `bin/install` to fix 123 | your configuration. 124 | 125 | Or you can force docker to always use the same IP by editing the 126 | `/etc/docker/daemon.json` file and adding: 127 | 128 | { 129 | "bip": "172.17.42.1/24" 130 | } 131 | 132 | **One more thing** When you start your host, the docker service is not fully 133 | loaded. 134 | Until this daemon is loaded, the dns container will not be automatically started 135 | and you will notice bad performance when your host will try to resolve DNS. 136 | The service is not fully loaded, because it uses a feature of systemd called 137 | [socket activation]: The first access to the docker socket will trigger the 138 | start of the true service. 139 | To skip this feature, you simply have to activate the docker service. 140 | 141 | sudo update-rc.d docker enable 142 | 143 | ## Troubleshooting 144 | 145 | To see the list of register DNS, dump the content of the generated 146 | `dnsmasq.conf` 147 | 148 | docker exec dns-gen cat /etc/dnsmasq.conf 149 | 150 | On restart, if you loose the dns resolution, check the `NetworkManager` service status. 151 | 152 | service NetworkManager status 153 | 154 | Check the syntax of the `/etc/resolv.conf` file which should contain at the 155 | begging the IP of the `docker0` interface: 156 | 157 | nameserver 172.17.42.1 158 | 159 | Check the containers logs 160 | 161 | docker logs --tail 100 -f dns-gen 162 | 163 | [docker-gen]: https://github.com/jwilder/docker-gen 164 | [socket activation]: http://0pointer.de/blog/projects/socket-activation.html 165 | [ansible playbook]: https://gist.github.com/jpic/7bfbe20cf759986b7c7c7851c2d63762 166 | -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | # This script is meant for quick & easy install via: 5 | # $ sudo sh -c "$(curl -fsSL https://raw.githubusercontent.com/jderusse/docker-dns-gen/master/bin/install)" 6 | 7 | C1='\033[0;31m' 8 | C2='\033[0;33m' 9 | C3='\033[0;32m' 10 | C4='\033[0;36m' 11 | C5='\033[0;35m' 12 | C0='\033[0m' 13 | 14 | if [ "$(id -u)" -ne 0 ]; then 15 | echo "${C1}This script must be run by root${C0}" 16 | echo "${C2} running ${C3}sudo $0 $@${C0}" 17 | sudo $0 "$@" 18 | exit 0 19 | fi 20 | 21 | GATEWAY=$(ip -4 addr show docker0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || echo "") 22 | main () { 23 | requirements 24 | install 25 | } 26 | 27 | requirements () { 28 | echo "${C2}Requirements${C0}" 29 | requirements_packages 30 | requirements_services 31 | requirements_config 32 | requirements_legacy 33 | requirements_network 34 | } 35 | 36 | requirements_packages () { 37 | install_if_needed "resolvconf" "resolvconf" "yes" 38 | echo "${C5} - resolvconf is installed${C0}" 39 | } 40 | 41 | requirements_services () { 42 | docker info 2>/dev/null >/dev/null || (echo "${C1} - docker should be installed and usable by root user${C0}" && exit 1) 43 | echo "${C5} - ${C3}docker${C5} is installed${C0}" 44 | 45 | docker rm -fv dns-gen > /dev/null 2> /dev/null || true 46 | echo "${C5} - container ${C3}dns-gen${C5} stoped${C0}" 47 | } 48 | 49 | requirements_config () { 50 | sed -i "/# dns-gen/d" /etc/resolvconf/resolv.conf.d/head 51 | sed -i "/nameserver ${GATEWAY}/d" /etc/resolvconf/resolv.conf.d/head 52 | echo "${C5} - ${C3}resolv.conf${C5} cleaned${C0}" 53 | } 54 | 55 | requirements_legacy () { 56 | previous=$(cat /etc/NetworkManager/dnsmasq.d/01_docker 2>/dev/null|grep "127.0.0.1#54" > /dev/null && echo "1" || echo "") 57 | if ! [ -z "${previous}" ]; then 58 | if confirm " Should I remove old dnsmasq.d/01_docker file?"; then 59 | rm /etc/NetworkManager/dnsmasq.d/01_docker 60 | service NetworkManager restart 61 | echo "${C5} - previous config ${C3}/etc/NetworkManager/dnsmasq.d/01_docker${C5} removed ${C0}" 62 | fi 63 | fi 64 | 65 | resolvconf -u 66 | } 67 | 68 | requirements_network () { 69 | ip -4 addr show docker0 2>/dev/null >/dev/null || (echo "${C1} - the docker0 interface does not exists${C0}" && exit 1) 70 | echo "${C5} - interface ${C3}docker0${C5} exists${C0}" 71 | 72 | used=$(nc -vz -u ${GATEWAY} 53 2>/dev/null && echo "1" || echo "") 73 | if [ -z "${used}" ]; then 74 | echo "${C5} - port ${C3}53${C5} is free${C0}" 75 | else 76 | echo "${C1} - port ${C3}53${C1} already used on ${C3}${GATEWAY}${C0}" 77 | exit 1 78 | fi 79 | } 80 | 81 | install () { 82 | echo "${C2}Install${C0}" 83 | install_container 84 | install_config 85 | } 86 | 87 | install_container () { 88 | docker pull jderusse/dns-gen:2 89 | docker run -d --name dns-gen \ 90 | --restart always \ 91 | --log-opt "max-size=10m" \ 92 | --net host \ 93 | -e GATEWAY=$GATEWAY \ 94 | --volume /:/host:ro \ 95 | --volume /var/run/docker.sock:/var/run/docker.sock \ 96 | jderusse/dns-gen:2 > /dev/null 97 | echo "${C5} - started container ${C3}dns-gen${C5}${C0}" 98 | } 99 | 100 | install_config () { 101 | # find insert position 102 | index=$(grep -n "^[^#;]" /etc/resolvconf/resolv.conf.d/head|head -n1|cut -d: -f1) 103 | line="nameserver ${GATEWAY} # dns-gen" 104 | if [ -z "${index}" ]; then 105 | echo "${line}" >> /etc/resolvconf/resolv.conf.d/head 106 | echo "${C5} - appended ${C3}${line}${C5} in ${C3}/etc/resolvconf/resolv.conf.d/head${C0}" 107 | else 108 | sed -i "${index}i${line}" /etc/resolvconf/resolv.conf.d/head 109 | echo "${C5} - inserted ${C3}${line}${C5} at position ${C3}${index}${C5} in ${C3}/etc/resolvconf/resolv.conf.d/head${C0}" 110 | fi 111 | 112 | resolvconf -u 113 | } 114 | 115 | install_if_needed () { 116 | command=${1} 117 | package=${2} 118 | reboot=${3:-""} 119 | if ! [ -x "$(command -v $1)" ]; then 120 | install_apt "${package}" 121 | if ! [ -z "${reboot}" ]; then 122 | echo "" 123 | echo " ${C1}+--------------------------------------------+" 124 | echo " ${C1}| |${C0}" 125 | echo " ${C1}| You probably need to to reboot your system |${C0}" 126 | echo " ${C1}| |${C0}" 127 | echo " ${C1}+--------------------------------------------+" 128 | echo "" 129 | fi 130 | fi 131 | } 132 | 133 | install_apt () { 134 | package=${1} 135 | echo " ${C1}This script requires ${C3}${package}${C0}" 136 | if confirm " May I install it for you?"; then 137 | apt-get update 138 | apt-get install "${package}" 139 | else 140 | echo " ${C1}Aborted${C0}" 141 | exit 1 142 | fi 143 | } 144 | 145 | confirm () { 146 | message=$1 147 | echo "${C4}${message}${C0}" 148 | while 149 | read -p " " answer 150 | do 151 | case $answer in 152 | ([yY][eE][sS] | [yY]) return 0;; 153 | ([nN][oO] | [nN]) return 1;; 154 | (*) echo "${C4}${message}${C3} yes ${C4}or${C3} no${C0}";; 155 | esac 156 | done 157 | } 158 | 159 | main 160 | -------------------------------------------------------------------------------- /docker-files/etc/dnsmasq.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "host" }} 2 | {{ $host := .Host }} 3 | {{ $tld := .Tld }} 4 | {{ if eq $tld "" }} 5 | {{ range $index, $network := .Container.Networks }} 6 | {{ if ne $network.IP "" }} 7 | address=/{{ $host }}/{{ $network.IP }} 8 | {{ end }} 9 | {{ end }} 10 | {{ else }} 11 | {{ range $index, $network := .Container.Networks }} 12 | {{ if ne $network.IP "" }} 13 | address=/{{ $host }}.{{ $tld }}/{{ $network.IP }} 14 | address=/{{ $host }}.{{ $network.Name }}.{{ $tld }}/{{ $network.IP }} 15 | {{ end }} 16 | {{ end }} 17 | {{ end }} 18 | {{ end }} 19 | 20 | {{ $tld := or ($.Env.DOMAIN_TLD) "docker" }} 21 | {{ range $index, $container := $ }} 22 | {{ $hosts := coalesce $container.Name (print "*." $container.Name) }} 23 | {{ $host_part := split $container.Name "_" }} 24 | {{ $host_part_len := len $host_part }} 25 | {{ if eq $host_part_len 3 }} 26 | {{ template "host" (dict "Container" $container "Host" (print (index $host_part 0)) "Tld" $tld) }} 27 | {{ template "host" (dict "Container" $container "Host" (print (index $host_part 1) "." (index $host_part 0)) "Tld" $tld) }} 28 | {{ end }} 29 | {{ if eq $host_part_len 4 }} 30 | {{ template "host" (dict "Container" $container "Host" (print (index $host_part 0)) "Tld" $tld) }} 31 | {{ template "host" (dict "Container" $container "Host" (print (index $host_part 1) "." (index $host_part 0)) "Tld" $tld) }} 32 | {{ template "host" (dict "Container" $container "Host" (print (index $host_part 2) "." (index $host_part 1) "." (index $host_part 0)) "Tld" $tld) }} 33 | {{ end }} 34 | {{ template "host" (dict "Container" $container "Host" $container.Name "Tld" $tld) }} 35 | {{ end }} 36 | 37 | {{ range $host, $containers := groupByMulti $ "Env.DOMAIN_NAME" "," }} 38 | {{ range $index, $container := $containers }} 39 | {{ template "host" (dict "Container" $container "Host" (print $host) "Tld" "") }} 40 | {{ end }} 41 | {{ end }} 42 | 43 | bind-interfaces 44 | interface=docker0 45 | except-interface=lo 46 | 47 | local=/docker/ 48 | resolv-file=/etc/resolv.dnsmasq 49 | -------------------------------------------------------------------------------- /docker-files/usr/local/bin/dnsmasq-reload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | killall dnsmasq 4 | 5 | # Give enough time to terminate the process. 6 | # 7 | sleep 1 8 | 9 | /usr/sbin/dnsmasq "$@" 10 | -------------------------------------------------------------------------------- /docker-files/usr/local/bin/entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "Ignoring server ${GATEWAY}" 4 | 5 | docker-gen -watch -only-exposed -notify "dnsmasq-reload -u root $*" /etc/dnsmasq.tmpl /etc/dnsmasq.conf & 6 | 7 | dnsmasq-reload -u root $* 8 | 9 | while true; do 10 | sed "/${GATEWAY}/d" "/host$(chroot /host realpath -L /etc/resolv.conf)" > /etc/resolv.dnsmasq 11 | sleep 1 12 | done 13 | --------------------------------------------------------------------------------