Docker expose service

devops docker - theimpossiblecode.com

Docker is a great tool for DevOps, and here we’re going to make it even better! We’re going to use a simple but brilliant solution to automatically expose selected services (containers) whenever they are started. No complicated machine allocation is needed – just docker/docker-compose and a simple one time system change for your Linux.

Docker expose service

We all know we can expose docker ports, so why not services? I’ve been using docker for a while now, and have become used to pulling container IPs to connect  to the services in them. Yes – you can export ports and connect to your local host at a specific port, but I find this somewhat awkward, and it doesn’t always work for what I need. Looking around for a solution, I didn’t find anything quite satisfying.  The only thing which came close was this post, and I’m basing my solution on it.

The requirements

Expose by name which represents an IP

This goes almost without saying, since this is the main issue to solve here. It is expected to work much like a dynamic DNS service, in which the IP changes, but the name remains the same.

Selection mechanism

I want to choose which container to expose, and not in its build, since the same image could be exposed several times as services with different names.

Mapping mechanism

The container ‘hostname’ or ‘service’ names are not necessarily the names I want to use externally.

Domain and subdomain support

The docker-compose.yml can set up a small network domain with subdomain hosts, which need external access. Actually sometimes multiple subdomains can connect us to the same host, something which is common in website development.

Docker encapsulation

The selective configuration should be part of the docker-compose YAML file.

Automation

  • For each container started or stopped we want automatic detection if it needs to be exposed externally.
  • This service should be a registered system service and started at boot time.

The implementation

The implementation is based on dnsmasq, docker events, the docker LABEL feature and a small bash script. The script which will be presented shortly identifies these labels:

  • com.theimpossiblecode.expose.host “name”
    • Here you are exposing the IP with the name.
    • If you have a domainname, then it will be appended to the hostname.
  • com.theimpossiblecode.expose.domain “domain.suffix”
    • If you want to reach the service with a domain name.
    • This will override your own external domainname.
    • You can reach the exposed host from above (if provided) with this domain suffix as well.
  • com.theimpossiblecode.expose.subdomainHosts “name [name…]”
    • These names will be appended the domain of your external host or the exposed domain name from above (overrides your own external domain).

Some or all of the above will cause the script to create a ‘hosts‘ entry for the dnsmasq, in a file which it is configured to use. After each such change the dnsmasq will be signaled to reload its config.

To install dnsmasq

sudo apt-get install dnsmasq

Configure dnsmasq for docker

Put this in /etc/dnsmasq.d/docker-dns:

addn-hosts=/etc/docker-container-hosts
interface=docker0

Restart dnsmasq:

sudo systemctl restart dnsmasq.service

The bash script

Put this in /usr/local/bin/update-docker-dns.sh:

#!/bin/bash

# Path to the addn-hosts file
CONTAINER_HOSTS=/etc/docker-container-hosts

DOMAIN_NAME=`domainname`
if [ `echo  "$DOMAIN_NAME" | grep -v '\.'` ]; then
    DOMAIN_NAME=""
fi

ECHO="/bin/echo"
generate_docker_hosts ()
{ 
    $ECHO "# Auto-generated by $0" > $CONTAINER_HOSTS
    for cid in `docker ps -q`; do
        host=`docker inspect --format='{{ index .Config.Labels "com.theimpossiblecode.expose.host"}}' $cid`
        container_domain=`docker inspect --format='{{ index .Config.Labels "com.theimpossiblecode.expose.domain"}}' $cid`
        subdomain_hosts=`docker inspect --format='{{ index .Config.Labels "com.theimpossiblecode.expose.subdomainHosts"}}' $cid`
        if [ "$host" != "" -o "$container_domain" != "" -o "$subdomain_hosts" != "" ]; then
            ip=`docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $cid`
            $ECHO -n "$ip  " >> $CONTAINER_HOSTS
            if [ "$container_domain" = "" ]; then
                container_domain=$DOMAIN_NAME  
            elif [ "$container_domain" != "$DOMAIN_NAME" ]; then
                $ECHO -n "$container_domain " >> $CONTAINER_HOSTS
            fi
            subdomain_hosts="$host $subdomain_hosts"  
            for host in $subdomain_hosts; do
                $ECHO -n " $host" >> $CONTAINER_HOSTS
                if [ "$container_domain" != "" ]; then
                    $ECHO -n " $host.$container_domain" >> $CONTAINER_HOSTS
                fi
            done
            $ECHO  >> $CONTAINER_HOSTS
        fi
    done

    # Ask dnsmasq to reload
    pkill -x -HUP dnsmasq
}

# Handle race conditions when dnsmasq is started before docker0 interface was up
if ! docker network ls ; then
    # Wait for docker networks to start
    while ! docker network ls ; do
        sleep 1
    done
    systemctl restart dnsmasq.service
fi

# Listen on docker events to maintain the CONTAINER_HOSTS file
coproc docker events --filter 'event=start' --filter 'event=stop'

# Initialize the CONTAINER_HOSTS now
`generate_docker_hosts`

while read -u ${COPROC[0]} line; do
    `generate_docker_hosts`
done

Make sure it is executable:

sudo chmod +x /usr/local/bin/update-docker-dns.sh

Set up the script as a service

Put this in /etc/systemd/system/docker-dnsmasq.service:

[Unit]
Description=Update dnsmasq with selected docker containers
After=docker.service dnsmasq.service

[Service]
ExecStart=/usr/local/bin/update-docker-dns.sh

[Install]
WantedBy=multi-user.target

Enable and start the service

sudo systemctl enable docker-dnsmasq.service
sudo systemctl start docker-dnsmasq.service

Test it

Use this docker-compose.yml based on https://docs.docker.com/compose/wordpress/:

version: '3'

services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

   wordpress:
     # We'll want to use a name to access this service, regardless of its IP
     labels:
       com.theimpossiblecode.expose.host: "dockerwp"
     depends_on:
       - db
     image: wordpress:latest
     # No need to map the ports anymore
     # ports:
     #  - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
volumes:
    db_data:

start it with:

 docker-compose up -d

Check the DNS lookup with dig:

dig dockerwp
; <<>> DiG 9.10.3-P4-Ubuntu <<>> dockerwp
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42941
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1280
;; QUESTION SECTION:
;dockerwp.			IN	A

;; ANSWER SECTION:
dockerwp.		0	IN	A	172.18.0.2

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Nov 19 15:59:37 UTC 2017
;; MSG SIZE  rcvd: 53

You can also open your browser now at http://dockerwp

Now bring down this setup with:

docker-compose down

And try again the DNS lookup to make sure nothing is found:

dig dockerwp
; <<>> DiG 9.10.3-P4-Ubuntu <<>> dockerwp
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 26781
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1280
;; QUESTION SECTION:
;dockerwp.			IN	A

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Nov 19 16:04:58 UTC 2017
;; MSG SIZE  rcvd: 37

Summary

  • This is a simple way to both selectively and automatically add, by name, small docker services to your system for whatever needs you have. This adds a very powerful tool to your DevOps  toolbox.
  • I’m interested to know if you found an easier way to cope with these requirements or if you find a problem with this setup. Please share.

See you next time,
Sagi.

Sagi Zeevi

A software developer brain surgeon … if software only had a brain.
An electronics hobbyist heart surgeon … if electronics only had a heart.

You may also like...

Leave a Reply

Scroll Up