Michael Crosby


Docker Events

The proper way to do port allocation in docker is to EXPOSE a private port in a container and let docker map the public port on the host. This can be a pain for reverse proxies, load balances, etc but with the events endpoint in docker you can listen for containers to start and inspect their port mappings.

Here is a very simple Go app as an example on how to utilize this endpoint.

Code:

The first thing that we want to do is to add a few imports.

package main

import (
    "encoding/json"
    "io"
    "log"
    "net/http"
)

We will need json for decoding the information from the api, io for io.EOF, log to print to stdout, and http for the Client.

Now lets create a few types to store our information.

type Event struct {
    Id     string `json:"id"`
    Status string `json:"status"`
}

type Config struct {
    Hostname string
}

type NetworkSettings struct {
    IpAddress   string
    PortMapping map[string]map[string]string
}

type Container struct {
    Id              string
    Image           string
    Config          *Config
    NetworkSettings *NetworkSettings
}

The api will return much more container information but I only care about a few fields and types in this example.

func main() {
    c := http.Client{}
    res, err := c.Get("http://localhost:4243/events")
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()

    // Read the streaming json from the events endpoint
    // http://docs.docker.io/en/latest/api/docker_remote_api_v1.3/#monitor-docker-s-events
    d := json.NewDecoder(res.Body)
    for {
        var event Event
        if err := d.Decode(&event); err != nil {
            if err == io.EOF {
                break
            }
            log.Fatal(err)
        }
        if event.Status == "start" {
            // We only want to inspect the container if it has started
            if container := inspectContainer(event.Id, c); container != nil {
                notify(container)
            }
        }
    }
}

This is my main() func. We create a new http.Client that will be used to make GET requests to the docker api. The first request that we make is to the events endpoint. This will open a stream to our app writing json objects when events happen in the docker daemon. We create a new json decoder to read from the Body of the response and decode Event types. After we decode the event we only care if the container has been started so we use the status field.

func inspectContainer(id string, c http.Client) *Container {
    // Use the container id to fetch the container json from the Remote API
    // http://docs.docker.io/en/latest/api/docker_remote_api_v1.4/#inspect-a-container
    res, err := c.Get("http://localhost:4243/containers/" + id + "/json")
    if err != nil {
        log.Println(err)
        return nil
    }
    defer res.Body.Close()

    if res.StatusCode == http.StatusOK {
        d := json.NewDecoder(res.Body)

        var container Container
        if err = d.Decode(&container); err != nil {
            log.Fatal(err)
        }
        return &container
    }
    return nil
}

Now that a container has started we use the id field from the event to then make another request to the docker api for the container information. We use another json decoder but this time we decode the json result returned from the api into the Container type.

func notify(container *Container) {
    settings := container.NetworkSettings

    if settings != nil && settings.PortMapping != nil {
        // I only care about Tcp ports but you can also view Udp mappings
        if ports, ok := settings.PortMapping["Tcp"]; ok {

            log.Printf("Ip address allocated for: %s", container.Id)

            // Log the public and private port mappings
            for privatePort, publicPort := range ports {
                // I am just writing to stdout but you can use this information to update hipache, redis, etc...
                log.Printf("%s -> %s", privatePort, publicPort)
            }
        }
    }
}

So now that we have the container information that we care about we need to get that data and make it useful. All we need to do is to look at the NetworkSettings for the PortMappings to inspect Tcp ports. You will receive a map where the key is the private port and the value is the public port. I am just writing the values to stdout but you could use this data to update hipache for a new container, write nginx config changes, etc... The possibilities are endless.

Let me know if you have any questions or comments below.

Full code:

package main

import (
    "encoding/json"
    "io"
    "log"
    "net/http"
)

type Event struct {
    Id     string `json:"id"`
    Status string `json:"status"`
}

type Config struct {
    Hostname string
}

type NetworkSettings struct {
    IpAddress   string
    PortMapping map[string]map[string]string
}

type Container struct {
    Id              string
    Image           string
    Config          *Config
    NetworkSettings *NetworkSettings
}

func inspectContainer(id string, c http.Client) *Container {
    // Use the container id to fetch the container json from the Remote API
    // http://docs.docker.io/en/latest/api/docker_remote_api_v1.4/#inspect-a-container
    res, err := c.Get("http://localhost:4243/containers/" + id + "/json")
    if err != nil {
        log.Println(err)
        return nil
    }
    defer res.Body.Close()

    if res.StatusCode == http.StatusOK {
        d := json.NewDecoder(res.Body)

        var container Container
        if err = d.Decode(&container); err != nil {
            log.Fatal(err)
        }
        return &container
    }
    return nil
}

func notify(container *Container) {
    settings := container.NetworkSettings

    if settings != nil && settings.PortMapping != nil {
        // I only care about Tcp ports but you can also view Udp mappings
        if ports, ok := settings.PortMapping["Tcp"]; ok {

            log.Printf("Ip address allocated for: %s", container.Id)

            // Log the public and private port mappings
            for privatePort, publicPort := range ports {
                // I am just writing to stdout but you can use this information to update hipache, redis, etc...
                log.Printf("%s -> %s", privatePort, publicPort)
            }
        }
    }
}

func main() {
    c := http.Client{}
    res, err := c.Get("http://localhost:4243/events")
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()

    // Read the streaming json from the events endpoint
    // http://docs.docker.io/en/latest/api/docker_remote_api_v1.3/#monitor-docker-s-events
    d := json.NewDecoder(res.Body)
    for {
        var event Event
        if err := d.Decode(&event); err != nil {
            if err == io.EOF {
                break
            }
            log.Fatal(err)
        }
        if event.Status == "start" {
            // We only want to inspect the container if it has started
            if container := inspectContainer(event.Id, c); container != nil {
                notify(container)
            }
        }
    }
}
comments powered by Disqus