4

Preface

I want to write a universal shell-script which will add a proper IP address of docker host machine to /etc/hosts inside of container.

You will probably advice me to put the service I want to access in another docker container too but for fastest tests on CI I want to access docker host machine by ip from container, that's why I need it.

There was an issue on github with lot's of comments where people were asking to make unified way to access host ip. There is no common solution for all platforms atm.

People make scripts like this:

grep dockerhost /etc/hosts || echo $(ip r | grep ^default | cut -d" " -f3) dockerhost >> /etc/hosts

In order to access docker host machine I would like to use the same approach but it does not work for Mac atm. The result returned by:

ip r | grep ^default | cut -d" " -f3

will give a wrong answer 172.17.0.1 if Docker host machine is Mac (You won't be able actually to access your Mac from container with 172.17.0.1).

That's why there is a feature in Docker for Mac - special hostname docker.for.mac.localhost with which you can access your host Mac from container.

The problem

I would like to write a universal shell script which will first try to resolve docker.for.mac.localhost and add the resolved address to /etc/hosts. If not (which means docker host is either windows or linux, not Mac), use the ip r | grep ^default | cut -d" " -f3 approach and add to /etc/hosts too.

The question is: How can I try to resolve docker.for.mac.localhost in ANY container? (some distros do not have nslookup for example). Or maybe I should not try to resolve and I can just try to read docker.for.mac.localhost value from somewhere?

My current thoughts are to try to use getent ahosts docker.for.mac.localhost, then in case command does not exist I will try to use nslookup (i.e. Busybox image does not have getent command). Probably I am wrong trying to resolve docker.for.mac.localhost and it can be retrieved in easier way. Please share your thoughts.

Thank you.

2

Your idea of resolving docker.for.mac.localhost is pretty well right-on-target, you just need to find something capable of resolving it in pretty much any container. This should do it (and has, according to my tests):

grep dockerhost /etc/hosts || echo $( (ping -c1 docker.for.mac.localhost || (ip r | grep ^default)) | grep -oE '([0-9]+\.){3}[0-9]+' ) dockerhost >> /etc/hosts

You can put that as part of your entrypoint (sh -c) or in an init script of some sort.

For more info, a more complete commented version, and the caveats, read on.

All we're doing is getting Linux to resolve the name. Ping is just the easiest choice, though nc and traceroute exist pretty much everywhere. If all else fails, you can actually do this with a modern (but non-BusyBox) bash or syslog. By default, Docker sets up a small DNS server that responds to the names of any other containers on the same network (or, in older versions, linked containers). And then docker.for.mac.localhost is added into that.

If nothing resolves it, then that dns server didn't resolve it, and we get the host normally. You may wish to use the /proc/net method instead, but they seem to be equivalent in my test containers.

Now, some caveats. First, a big one: if you have no network connection, this will take forever to timeout - up to 30 seconds. There are some fixes in the larger script below. You also can't (well, by default - there are ways to override this) run this during the container's build. Rather, you can, it just won't do anything: Docker mounts a new /etc/hosts when you start the container.

A better version would use timeout, but you run into a problem - there are (at least) 2 incompatible versions of it, and a third case if you want to assume it might not be there:

grep dockerhost /etc/hosts || echo $( (
(if timeout -t1 env >/dev/null; then \
     timeout -t1 ping -c1 docker.for.mac.localhost; \
   elif timeout 1s env >/dev/null; then
      timeout 1s ping -c1 docker.for.mac.localhost; \
   else \
      ping -c1 docker.for.mac.localhost; \
fi) \
|| (ip r | grep ^default)) \
| grep -oE '([0-9]+\.){3}[0-9]+' ) dockerhost >> /etc/hosts

We use env because there is almost certainly a version that isn't a shell built-in (/usr/bin/env in most cases), it has a well-known interface (ie, it will run without any input), and it's unlikely to fail for environmental reason unlike pesky commands like ls, which depend on a filesystem.

And finally, a complete commented version similar to what I'm using (so that people that come behind me understand what's going on):

add_dockerhost_address() {
  # The name we're checking for - in case this technique is useful on Windows 
  # if we get a Windows version instead of a cross-platform one
  local MACHOST="docker.for.mac.localhost"
  # The name we want to insert into /etc/hosts
  local DOCKERHOSTNAME="dockerhost"

  # Don't insert the name twice
  if grep "$DOCKERHOSTNAME" /etc/hosts >/dev/null 2>&1; then
    return 0
  fi

  # A command string we'll be building
  local COMMAND=""

  # Check for the ability to limit the timeout
  if command -v timeout >/dev/null 2>&1; then
    # We still have a problem - there are two widely used incompatible
    # variants of timeout. Let's see which one we have.

    # env should not be a builtin, doesn't require a pipe,
    # has a consistent command line test, and is unlikely to fail
    if timeout -t1 env >/dev/null 2>&1; then
      COMMAND="timeout -t1 "
    else
      COMMAND="timeout 1s "
    fi
  fi

  local IS_NOT_MAC=1
  local IP=""

  if command -v ping >/dev/null 2>&1; then
    COMMAND="$COMMAND ping -c1 $MACHOST"
    # Otherwise, BusyBox shows terminated.
    IP=$(sh -c "$COMMAND" 2>&1)
    # Exit codes: 0 - found the host, 1/2 failed to find, 124/143 terminated
    IS_NOT_MAC="$?"
  fi

  if [ "$IS_NOT_MAC" -eq 0 ]; then
    IP=$(echo "$IP" | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')    
  else
    IP=$(ip r | grep ^default | cut -d" " -f3)
  fi

  echo "$IP $DOCKERHOSTNAME" >> /etc/hosts
}

add_dockerhost_address

exec "/usr/sbin/apachectl" "-DFOREGROUND"
1

I believe it is mentioned here that docker.for.mac.host.internal can be used. There is no mention of docker.for.mac.localhost, even though it also works for me.

Then, I use the following line to check if I am running on Docker for Mac or not:

$ getent hosts docker.for.mac.host.internal

$? tells me if docker.for.mac.host.internal was found.

To get the actual IP, one can also use awk, like so:

$ getent hosts docker.for.mac.host.internal | awk '{ print $1 }'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.