From 948d7902f22c60dd9ba952306f8a6d75a7efe729 Mon Sep 17 00:00:00 2001 From: Michael Hobbs Date: Fri, 2 Jan 2015 16:06:32 -0800 Subject: [PATCH 1/3] bind docker container to internal port if using vhosts --- dokku | 20 ++++++++-- plugins/nginx-vhosts/bind-external-ip | 21 ++++++++++ plugins/nginx-vhosts/commands | 4 +- plugins/nginx-vhosts/post-deploy | 4 +- tests/unit/ports.bats | 57 +++++++++++++++++++++++++++ tests/unit/test_helper.bash | 1 + 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100755 plugins/nginx-vhosts/bind-external-ip create mode 100644 tests/unit/ports.bats diff --git a/dokku b/dokku index 9374b6e6f..f59d247f0 100755 --- a/dokku +++ b/dokku @@ -82,8 +82,20 @@ case "$1" in # start the app DOCKER_ARGS=$(: | pluginhook docker-args $APP deploy) - id=$(docker run -d -p 5000 -e PORT=5000 $DOCKER_ARGS $IMAGE /bin/bash -c "/start web") - port=$(docker port $id 5000 | sed 's/[0-9.]*://') + BIND_EXTERNAL=$(pluginhook bind-external-ip $APP) + + if [[ "$BIND_EXTERNAL" = "false" ]];then + port=5000 + id=$(docker run -d -e PORT=$port $DOCKER_ARGS $IMAGE /bin/bash -c "/start web") + ipaddr=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' $id) + + echo $ipaddr > "$DOKKU_ROOT/$APP/IP" + else + id=$(docker run -d -p 5000 -e PORT=5000 $DOCKER_ARGS $IMAGE /bin/bash -c "/start web") + port=$(docker port $id 5000 | sed 's/[0-9.]*://') + + [[ -f "$DOKKU_ROOT/$APP/IP" ]] && rm -f "$DOKKU_ROOT/$APP/IP" + fi # if we can't post-deploy successfully, kill new container function kill_new { @@ -95,7 +107,7 @@ case "$1" in # run checks first, then post-deploy hooks, which switches Nginx traffic trap kill_new INT TERM EXIT echo "-----> Running pre-flight checks" - pluginhook check-deploy $id $APP $port + pluginhook check-deploy $id $APP $port ${ipaddr:-localhost} # now using the new container echo $id > "$DOKKU_ROOT/$APP/CONTAINER" @@ -103,7 +115,7 @@ case "$1" in echo "http://$(< "$DOKKU_ROOT/HOSTNAME"):$port" > "$DOKKU_ROOT/$APP/URL" echo "-----> Running post-deploy" - pluginhook post-deploy $APP $port + pluginhook post-deploy $APP $port $ipaddr trap - INT TERM EXIT # kill the old container diff --git a/plugins/nginx-vhosts/bind-external-ip b/plugins/nginx-vhosts/bind-external-ip new file mode 100755 index 000000000..55159289e --- /dev/null +++ b/plugins/nginx-vhosts/bind-external-ip @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +APP="$1" + +set +e; NO_VHOST=$(dokku config:get $APP NO_VHOST); set -e +if [[ -f "$DOKKU_ROOT/VHOST" ]];then + GLOBAL_VHOST=$(< "$DOKKU_ROOT/VHOST") +else + GLOBAL_VHOST=$(< "$DOKKU_ROOT/HOSTNAME") +fi + +if [[ -n "$NO_VHOST" ]]; then + echo true # bind to external ip. VHOST is disabled for this app +elif [[ "$GLOBAL_VHOST" =~ ([0-9]{1,3}[\.]){3}[0-9]{1,3} ]];then + echo true # bind to external ip. GLOBAL_VHOST is somehow an IP +elif [[ -f "$DOKKU_ROOT/$APP/VHOST" ]]; then + echo false # bind to docker ip. this app has a vhost defined +else + echo false +fi diff --git a/plugins/nginx-vhosts/commands b/plugins/nginx-vhosts/commands index bb85ca607..8434f21ae 100755 --- a/plugins/nginx-vhosts/commands +++ b/plugins/nginx-vhosts/commands @@ -15,7 +15,7 @@ restart_nginx () { case "$1" in nginx:build-config) - APP="$2"; PORT="$3" + APP="$2"; PORT="$3"; IP="${4:-127.0.0.1}" [[ -z "$PORT" ]] && PORT=$(< "$DOKKU_ROOT/$APP/PORT") VHOST_PATH="$DOKKU_ROOT/$APP/VHOST" WILDCARD_SSL="$DOKKU_ROOT/tls" @@ -68,7 +68,7 @@ EOF NOSSL_SERVER_NAME=$(echo $NONSSL_VHOSTS $SSL_VHOSTS| tr '\n' ' ') echo "-----> Creating $SCHEME nginx.conf" - echo "upstream $APP { server 127.0.0.1:$PORT; }" > $DOKKU_ROOT/$APP/nginx.conf + echo "upstream $APP { server $IP:$PORT; }" > $DOKKU_ROOT/$APP/nginx.conf eval "cat <<< \"$(< $NGINX_CONF)\" >> $DOKKU_ROOT/$APP/nginx.conf" echo "-----> Running nginx-pre-reload" diff --git a/plugins/nginx-vhosts/post-deploy b/plugins/nginx-vhosts/post-deploy index cdf81d452..72516694b 100755 --- a/plugins/nginx-vhosts/post-deploy +++ b/plugins/nginx-vhosts/post-deploy @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x -APP="$1"; PORT="$2" +APP="$1"; PORT="$2"; IP="$3" set +e; NO_VHOST=$(dokku config:get $APP NO_VHOST); set -e @@ -11,4 +11,4 @@ elif [[ ! -f "$DOKKU_ROOT/$APP/VHOST" ]]; then dokku domains:setup $APP fi -dokku nginx:build-config $APP $PORT +dokku nginx:build-config $APP $PORT $IP diff --git a/tests/unit/ports.bats b/tests/unit/ports.bats new file mode 100644 index 000000000..76166ea53 --- /dev/null +++ b/tests/unit/ports.bats @@ -0,0 +1,57 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + [[ -f "$DOKKU_ROOT/VHOST" ]] && cp -f "$DOKKU_ROOT/VHOST" "$DOKKU_ROOT/VHOST.bak" + [[ -f "$DOKKU_ROOT/HOSTNAME" ]] && cp -f "$DOKKU_ROOT/HOSTNAME" "$DOKKU_ROOT/HOSTNAME.bak" + +} + +teardown() { + destroy_app + [[ -f "$DOKKU_ROOT/VHOST.bak" ]] && mv "$DOKKU_ROOT/VHOST.bak" "$DOKKU_ROOT/VHOST" + [[ -f "$DOKKU_ROOT/HOSTNAME.bak" ]] && mv "$DOKKU_ROOT/HOSTNAME.bak" "$DOKKU_ROOT/HOSTNAME" +} + +@test "port exposure (with global VHOST)" { + echo "dokku.me" > "$DOKKU_ROOT/VHOST" + deploy_app + CONTAINER_ID=$(docker ps --no-trunc| grep dokku/$TEST_APP | grep "start web" | awk '{ print $1 }') + run bash -c "docker port $CONTAINER_ID | sed 's/[0-9.]*://' | egrep '[0-9]*'" + echo "output: "$output + echo "status: "$status + assert_failure +} + +@test "port exposure (without global VHOST and real HOSTNAME)" { + rm "$DOKKU_ROOT/VHOST" + echo "dokku.me" > "$DOKKU_ROOT/HOSTNAME" + deploy_app + CONTAINER_ID=$(docker ps --no-trunc| grep dokku/$TEST_APP | grep "start web" | awk '{ print $1 }') + run bash -c "docker port $CONTAINER_ID | sed 's/[0-9.]*://' | egrep '[0-9]*'" + echo "output: "$output + echo "status: "$status + assert_failure +} + +@test "port exposure (with NO_VHOST set)" { + deploy_app + dokku config:set $TEST_APP NO_VHOST=1 + CONTAINER_ID=$(docker ps --no-trunc| grep dokku/$TEST_APP | grep "start web" | awk '{ print $1 }') + run bash -c "docker port $CONTAINER_ID | sed 's/[0-9.]*://' | egrep '[0-9]*'" + echo "output: "$output + echo "status: "$status + assert_success +} + +@test "port exposure (without global VHOST and ip as HOSTNAME)" { + rm "$DOKKU_ROOT/VHOST" + echo "127.0.0.1" > "$DOKKU_ROOT/HOSTNAME" + deploy_app + CONTAINER_ID=$(docker ps --no-trunc| grep dokku/$TEST_APP | grep "start web" | awk '{ print $1 }') + run bash -c "docker port $CONTAINER_ID | sed 's/[0-9.]*://' | egrep '[0-9]*'" + echo "output: "$output + echo "status: "$status + assert_success +} diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index d671ae1b6..d2b10dd47 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -1,6 +1,7 @@ #!/usr/bin/env bash # constants +DOKKU_ROOT=${DOKKU_ROOT:=~dokku} TEST_APP=my-cool-guy-test-app # test functions From 441d212ef2a0893ed912c53cb276b44871997f17 Mon Sep 17 00:00:00 2001 From: Michael Hobbs Date: Mon, 5 Jan 2015 10:47:20 -0800 Subject: [PATCH 2/3] add ipv6 detection for external port binding --- plugins/domains/commands | 17 ++++++++++++++++- plugins/nginx-vhosts/bind-external-ip | 21 ++++++++++++++++++--- tests/unit/ports.bats | 13 ++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/plugins/domains/commands b/plugins/domains/commands index 05f6758f6..9ae7bec38 100755 --- a/plugins/domains/commands +++ b/plugins/domains/commands @@ -1,6 +1,21 @@ #!/usr/bin/env bash set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +RE_IPV4="([0-9]{1,3}[\.]){3}[0-9]{1,3}" + +RE_IPV6="([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" # TEST: 1:2:3:4:5:6:7:8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,7}:|" # TEST: 1:: 1:2:3:4:5:6:7:: +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" # TEST: 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" # TEST: 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" # TEST: 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" # TEST: 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" # TEST: 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 +RE_IPV6="${RE_IPV6}[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" # TEST: 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 +RE_IPV6="${RE_IPV6}:((:[0-9a-fA-F]{1,4}){1,7}|:)|" # TEST: ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: +RE_IPV6="${RE_IPV6}fe08:(:[0-9a-fA-F]{1,4}){2,2}%[0-9a-zA-Z]{1,}|" # TEST: fe08::7:8%eth0 fe08::7:8%1 (link-local IPv6 addresses with zone index) +RE_IPV6="${RE_IPV6}::(ffff(:0{1,4}){0,1}:){0,1}${RE_IPV4}|" # TEST: ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses) +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}:${RE_IPV4}" # TEST: 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 + case "$1" in domains) [[ -z $2 ]] && echo "Please specify an app to run the command on" && exit 1 @@ -23,7 +38,7 @@ case "$1" in else VHOST=$(< "$DOKKU_ROOT/HOSTNAME") fi - if [[ "$VHOST" =~ ([0-9]{1,3}[\.]){3}[0-9]{1,3} ]];then + if [[ "$VHOST" =~ $RE_IPV4 ]] || [[ "$VHOST" =~ $RE_IPV6 ]];then echo "ip found as hostname. disabling vhost support" [[ ! $(grep -q NO_VHOST "$DOKKU_ROOT/$APP/ENV") ]] && echo "export NO_VHOST='1'" >> "$DOKKU_ROOT/$APP/ENV" else diff --git a/plugins/nginx-vhosts/bind-external-ip b/plugins/nginx-vhosts/bind-external-ip index 55159289e..636bc894c 100755 --- a/plugins/nginx-vhosts/bind-external-ip +++ b/plugins/nginx-vhosts/bind-external-ip @@ -2,8 +2,23 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x APP="$1" - set +e; NO_VHOST=$(dokku config:get $APP NO_VHOST); set -e + +RE_IPV4="([0-9]{1,3}[\.]){3}[0-9]{1,3}" + +RE_IPV6="([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" # TEST: 1:2:3:4:5:6:7:8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,7}:|" # TEST: 1:: 1:2:3:4:5:6:7:: +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" # TEST: 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" # TEST: 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" # TEST: 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" # TEST: 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" # TEST: 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 +RE_IPV6="${RE_IPV6}[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" # TEST: 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 +RE_IPV6="${RE_IPV6}:((:[0-9a-fA-F]{1,4}){1,7}|:)|" # TEST: ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: +RE_IPV6="${RE_IPV6}fe08:(:[0-9a-fA-F]{1,4}){2,2}%[0-9a-zA-Z]{1,}|" # TEST: fe08::7:8%eth0 fe08::7:8%1 (link-local IPv6 addresses with zone index) +RE_IPV6="${RE_IPV6}::(ffff(:0{1,4}){0,1}:){0,1}${RE_IPV4}|" # TEST: ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses) +RE_IPV6="${RE_IPV6}([0-9a-fA-F]{1,4}:){1,4}:${RE_IPV4}" # TEST: 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 + if [[ -f "$DOKKU_ROOT/VHOST" ]];then GLOBAL_VHOST=$(< "$DOKKU_ROOT/VHOST") else @@ -12,8 +27,8 @@ fi if [[ -n "$NO_VHOST" ]]; then echo true # bind to external ip. VHOST is disabled for this app -elif [[ "$GLOBAL_VHOST" =~ ([0-9]{1,3}[\.]){3}[0-9]{1,3} ]];then - echo true # bind to external ip. GLOBAL_VHOST is somehow an IP +elif [[ "$GLOBAL_VHOST" =~ $RE_IPV4 ]] || [[ "$GLOBAL_VHOST" =~ $RE_IPV6 ]]; then + echo true # bind to external ip. GLOBAL_VHOST is somehow an IPv4 or IPv6 address elif [[ -f "$DOKKU_ROOT/$APP/VHOST" ]]; then echo false # bind to docker ip. this app has a vhost defined else diff --git a/tests/unit/ports.bats b/tests/unit/ports.bats index 76166ea53..ebafd23a5 100644 --- a/tests/unit/ports.bats +++ b/tests/unit/ports.bats @@ -45,7 +45,7 @@ teardown() { assert_success } -@test "port exposure (without global VHOST and ip as HOSTNAME)" { +@test "port exposure (without global VHOST and IPv4 address as HOSTNAME)" { rm "$DOKKU_ROOT/VHOST" echo "127.0.0.1" > "$DOKKU_ROOT/HOSTNAME" deploy_app @@ -55,3 +55,14 @@ teardown() { echo "status: "$status assert_success } + +@test "port exposure (without global VHOST and IPv6 address as HOSTNAME)" { + rm "$DOKKU_ROOT/VHOST" + echo "fda5:c7db:a520:bb6d::aabb:ccdd:eeff" > "$DOKKU_ROOT/HOSTNAME" + deploy_app + CONTAINER_ID=$(docker ps --no-trunc| grep dokku/$TEST_APP | grep "start web" | awk '{ print $1 }') + run bash -c "docker port $CONTAINER_ID | sed 's/[0-9.]*://' | egrep '[0-9]*'" + echo "output: "$output + echo "status: "$status + assert_success +} From f1827a86a4d0def9dcd6c013baf8f6b728450502 Mon Sep 17 00:00:00 2001 From: Michael Hobbs Date: Mon, 5 Jan 2015 16:28:48 -0800 Subject: [PATCH 3/3] add docs for network interface binding logic --- docs/nginx.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/nginx.md b/docs/nginx.md index ca0039440..d309e2298 100644 --- a/docs/nginx.md +++ b/docs/nginx.md @@ -136,3 +136,25 @@ dokku domains:clear myapp # remove a custom domain from app dokku domains:remove myapp example.com ``` + +### Container network interface binding + +> New as of 0.3.13 + +The deployed docker container running your app's web process will bind to either the internal docker network interface (i.e. `docker inspect --format '{{ .NetworkSettings.IPAddress }}' $CONTAINER_ID`) or an external interface (i.e. 0.0.0.0) depending on dokku's VHOST configuration. Dokku will attempt to bind to the internal docker network interface unless you specifically set NO_VHOST for the given app or your dokku installation is not setup to use VHOSTS (i.e. $DOKKU_ROOT/VHOST or $DOKKU_ROOT/HOSTNAME is set to an IPv4 or IPv6 address) + +```shell +# container bound to docker interface +root@dokku:~/dokku# docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +1b88d8aec3d1 dokku/node-js-app:latest "/bin/bash -c '/star About a minute ago Up About a minute goofy_albattani + +root@dokku:~/dokku# docker inspect --format '{{ .NetworkSettings.IPAddress }}' goofy_albattani +172.17.0.6 + +# container bound to all interfaces (previous default) +root@dokku:/home/dokku# docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +d6499edb0edb dokku/node-js-app:latest "/bin/bash -c '/star About a minute ago Up About a minute 0.0.0.0:49153->5000/tcp nostalgic_tesla + +```