├── .gitignore ├── Dockerfile ├── Godeps ├── Makefile ├── README.md ├── hosts.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .godeps/ 2 | docker-hosts 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM blalor/centos:latest 2 | MAINTAINER Brian Lalor 3 | 4 | ADD release/docker-hosts /usr/local/bin/ 5 | 6 | ## should *not* run as root, but needs access to /var/run/docker.sock, which 7 | ## should *not* be accessible by nobody. *sigh* 8 | #USER nobody 9 | ENTRYPOINT ["/usr/local/bin/docker-hosts"] 10 | -------------------------------------------------------------------------------- /Godeps: -------------------------------------------------------------------------------- 1 | github.com/fsouza/go-dockerclient 4fc0e0dbba85f2f799aa77cea594d40267f5bd5a 2 | github.com/jessevdk/go-flags 4f0ca1e2d1349e9662b633ea1b8b8d48e8a32533 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=docker-hosts 2 | BIN=.godeps/bin 3 | 4 | SOURCES=main.go hosts.go 5 | 6 | .PHONY: all init build tools release clean 7 | 8 | all: build 9 | 10 | .godeps: 11 | gvp init 12 | gvp in gpm install 13 | 14 | init: .godeps 15 | mkdir -p stage 16 | 17 | build: stage/$(NAME) 18 | 19 | stage/$(NAME): init $(SOURCES) 20 | gvp in go build -v -o $@ ./... 21 | 22 | $(BIN)/gpm: init 23 | curl -s -L -o $@ https://github.com/pote/gpm/raw/v1.2.3/bin/gpm 24 | chmod +x $@ 25 | 26 | $(BIN)/gvp: init 27 | curl -s -L -o $@ https://github.com/pote/gvp/raw/v0.1.0/bin/gvp 28 | chmod +x $@ 29 | 30 | tools: $(BIN)/gpm $(BIN)/gvp 31 | 32 | release/$(NAME): tools $(SOURCES) 33 | docker run \ 34 | -i -t \ 35 | -v $(PWD):/gopath/src/app \ 36 | -w /gopath/src/app \ 37 | google/golang:1.3 \ 38 | $(BIN)/gvp in go build -v -o $@ ./... 39 | 40 | release: release/$(NAME) 41 | 42 | docker: release 43 | docker build --tag=blalor/$(NAME) . 44 | docker push blalor/$(NAME) 45 | 46 | clean: 47 | rm -rf stage release .godeps 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simplified Docker container hostname resolution 2 | 3 | `docker-hosts` (yes, a terrible name) maintains a file in the format of 4 | `/etc/hosts` that contains IP addresses and hostnames of Docker containers. When 5 | the generated file is mounted at `/etc/hosts` within your Docker container it 6 | provides simple hostname resolution. This allows you to set up `redis` and 7 | `web` containers where the `web` container is able to connect to `redis` via its 8 | hostname. You can optionally provide a domain like `dev.docker`, so 9 | `redis.dev.docker` is a usable alias, as well. 10 | 11 | This utility was inspired by Michael Crosby's 12 | [skydock](https://github.com/crosbymichael/skydock) project. `docker-hosts` and 13 | skydock (paired with skydns) work in much the same way: the container lifecycle 14 | is monitored via the Docker daemon's events, and resolvable hostnames are made 15 | available to appropriately-configured containers. The end result is that you 16 | have a simple way of connecting containers together on the same Docker host, 17 | without having to resort to links or manual configuration. This does *not* 18 | provide a solution to container connectivity across Docker hosts. For that you 19 | should look at something like Jeff Lindsay's 20 | [registrator](https://github.com/progrium/registrator). 21 | 22 | ## building 23 | 24 | This project uses [gpm][gpm] and [gvp][gvp]. Both must be available on your 25 | path. 26 | 27 | make 28 | 29 | -- or -- 30 | 31 | gvp init 32 | source gvp in 33 | gpm install 34 | go build -v -o stage/docker-host ./... 35 | 36 | ## running 37 | 38 | Start the `docker-host` process and give it the path to a file that will be 39 | mounted as `/etc/hosts` in your containers: 40 | 41 | docker-host /path/to/hosts 42 | 43 | Optionally specify `DOCKER_HOST` environment variable. 44 | 45 | Then start a container: 46 | 47 | docker run -i -t -v /path/to/hosts:/etc/hosts:ro centos /bin/bash 48 | 49 | Within the `centos` container, you'll see `/etc/hosts` has an entry for the 50 | container you just started, as well as any other containers already running. 51 | `/etc/hosts` will continue to reflect all of the containers currently running on 52 | this Docker host. 53 | 54 | The **only** container that should have write access to the generated hosts file 55 | is the container running this application. 56 | 57 | ## running in Docker 58 | 59 | Create an empty file at `/var/lib/docker/hosts`, make it mode `0644` and owned 60 | by `nobody:nobody`. 61 | 62 | docker run \ 63 | -d \ 64 | -v /var/run/docker.sock:/var/run/docker.sock \ 65 | -v /var/lib/docker/hosts:/srv/hosts \ 66 | blalor/docker-hosts --domain-name=dev.docker /srv/hosts 67 | 68 | [gpm]: https://github.com/pote/gpm 69 | [gvp]: https://github.com/pote/gvp 70 | -------------------------------------------------------------------------------- /hosts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | dockerapi "github.com/fsouza/go-dockerclient" 5 | "log" 6 | "os" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | type HostEntry struct { 12 | IPAddress string 13 | CanonicalHostname string 14 | Aliases []string 15 | } 16 | 17 | type Hosts struct { 18 | sync.Mutex 19 | docker *dockerapi.Client 20 | path string 21 | domain string 22 | entries map[string]HostEntry 23 | } 24 | 25 | func NewHosts(docker *dockerapi.Client, path, domain string) *Hosts { 26 | hosts := &Hosts{ 27 | docker: docker, 28 | path: path, 29 | domain: domain, 30 | } 31 | 32 | hosts.entries = make(map[string]HostEntry) 33 | 34 | // combination of docker, centos 35 | hosts.entries["__localhost4"] = HostEntry{ 36 | IPAddress: "127.0.0.1", 37 | CanonicalHostname: "localhost", 38 | Aliases: []string{"localhost4"}, 39 | } 40 | 41 | hosts.entries["__localhost6"] = HostEntry{ 42 | IPAddress: "::1", 43 | CanonicalHostname: "localhost", 44 | Aliases: []string{"localhost6", "ip6-localhost", "ip6-loopback"}, 45 | } 46 | 47 | // docker puts these in 48 | hosts.entries["fe00::0"] = HostEntry{"fe00::0", "ip6-localnet", nil} 49 | hosts.entries["ff00::0"] = HostEntry{"ff00::0", "ip6-mcastprefix", nil} 50 | hosts.entries["ff02::1"] = HostEntry{"ff02::1", "ip6-allnodes", nil} 51 | hosts.entries["ff02::2"] = HostEntry{"ff02::2", "ip6-allrouters", nil} 52 | 53 | return hosts 54 | } 55 | 56 | func (h *Hosts) WriteFile() { 57 | file, err := os.Create(h.path) 58 | 59 | if err != nil { 60 | log.Println("unable to write to", h.path, err) 61 | return 62 | } 63 | 64 | defer file.Close() 65 | 66 | for _, entry := range h.entries { 67 | // \t\t\t…\t\n 68 | file.WriteString(strings.Join( 69 | append( 70 | []string{entry.IPAddress, entry.CanonicalHostname}, 71 | entry.Aliases..., 72 | ), 73 | "\t", 74 | ) + "\n") 75 | } 76 | } 77 | 78 | func (h *Hosts) Add(containerId string) { 79 | h.Lock() 80 | defer h.Unlock() 81 | 82 | container, err := h.docker.InspectContainer(containerId) 83 | if err != nil { 84 | log.Println("unable to inspect container:", containerId, err) 85 | return 86 | } 87 | 88 | entry := HostEntry{ 89 | IPAddress: container.NetworkSettings.IPAddress, 90 | CanonicalHostname: container.Config.Hostname, 91 | Aliases: []string{ 92 | // container.Name[1:], // could contain "_" 93 | }, 94 | } 95 | 96 | if h.domain != "" { 97 | entry.Aliases = 98 | append(h.entries[containerId].Aliases, container.Config.Hostname+"."+h.domain) 99 | } 100 | 101 | h.entries[containerId] = entry 102 | 103 | h.WriteFile() 104 | } 105 | 106 | func (h *Hosts) Remove(containerId string) { 107 | h.Lock() 108 | defer h.Unlock() 109 | 110 | delete(h.entries, containerId) 111 | 112 | h.WriteFile() 113 | } 114 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "log" 6 | 7 | flags "github.com/jessevdk/go-flags" 8 | dockerapi "github.com/fsouza/go-dockerclient" 9 | ) 10 | 11 | func getopt(name, def string) string { 12 | if env := os.Getenv(name); env != "" { 13 | return env 14 | } 15 | 16 | return def 17 | } 18 | 19 | func assert(err error) { 20 | if err != nil { 21 | log.Fatal("docker-hosts: ", err) 22 | } 23 | } 24 | 25 | type Options struct { 26 | DomainName string `short:"d" long:"domain-name" description:"domain to append"` 27 | File struct { 28 | Filename string 29 | } `positional-args:"true" required:"true" description:"the hosts file to write"` 30 | } 31 | 32 | func main() { 33 | var opts Options 34 | 35 | _, err := flags.Parse(&opts) 36 | if err != nil { 37 | os.Exit(1) 38 | } 39 | 40 | docker, err := dockerapi.NewClient(getopt("DOCKER_HOST", "unix:///var/run/docker.sock")) 41 | assert(err) 42 | 43 | hosts := NewHosts(docker, opts.File.Filename, opts.DomainName) 44 | 45 | // set up to handle events early, so we don't miss anything while doing the 46 | // initial population 47 | events := make(chan *dockerapi.APIEvents) 48 | assert(docker.AddEventListener(events)) 49 | 50 | containers, err := docker.ListContainers(dockerapi.ListContainersOptions{}) 51 | assert(err) 52 | 53 | for _, listing := range containers { 54 | go hosts.Add(listing.ID) 55 | } 56 | 57 | log.Println("docker-hosts: Listening for Docker events...") 58 | for msg := range events { 59 | switch msg.Status { 60 | case "start": 61 | go hosts.Add(msg.ID) 62 | 63 | case "die": 64 | go hosts.Remove(msg.ID) 65 | } 66 | } 67 | 68 | log.Fatal("docker-hosts: docker event loop closed") // todo: reconnect? 69 | } 70 | --------------------------------------------------------------------------------