diff --git a/docs/deployment/dockerfiles.md b/docs/deployment/dockerfiles.md index 421a69c34ae..fe9c9eb0b64 100644 --- a/docs/deployment/dockerfiles.md +++ b/docs/deployment/dockerfiles.md @@ -39,3 +39,33 @@ dokku config:set APP DOKKU_DOCKERFILE_START_CMD="--harmony server.js" To tell docker what to run. Setting `$DOKKU_DOCKERFILE_CACHE_BUILD` to `true` or `false` will enable or disable docker's image layer cache. Lastly, for more granular build control, you may also pass any `docker build` option to `docker`, by setting `$DOKKU_DOCKER_BUILD_OPTS`. + +### Procfiles and Multiple Processes + +You can also customize the run command using a `Procfile`, much like you would on Heroku or +with a buildpack deployed app. The `Procfile` should contain one or more lines defining [process +types and associated commands](https://devcenter.heroku.com/articles/procfile#declaring-process-types). +When you deploy your app a Docker image will be built, the `Procfile` will be extracted from the image +(it must be in the folder defined in your `Dockerfile` as `WORKDIR` or `/app`) and the commands +in it will be passed to `docker run` to start your process(es). Here's an example `Procfile`: + +``` +web: bin/run-prod.sh +worker: bin/run-worker.sh +``` + +And `Dockerfile`: + +``` +FROM debian:jessie +WORKDIR /app +COPY . ./ +CMD ["bin/run-dev.sh"] +``` + +When you deploy this app the `web` process will automatically be scaled to 1 and your Docker container +will be started basically using the command `docker run bin/run-prod.sh`. If you want to also run +a worker container for this app, you can run `dokku ps:scale worker=1` and a new container will be +started by running `docker run bin/run-worker.sh` (the actual `docker run` commands are a bit more +complex, but this is the basic idea). If you use an `ENTRYPOINT` in your `Dockerfile`, the lines +in your `Procfile` will be passed as arguments to the `ENTRYPOINT` script instead of being executed. diff --git a/dokku b/dokku index 186441f3993..f69e4c60ffc 100755 --- a/dokku +++ b/dokku @@ -137,7 +137,9 @@ case "$1" in if [[ -z "$DOKKU_HEROKUISH" ]]; then DOKKU_DOCKERFILE_PORT=$(dokku config:get $APP DOKKU_DOCKERFILE_PORT || true) - START_CMD=$(dokku config:get $APP DOKKU_DOCKERFILE_START_CMD || $START_CMD) + DOKKU_DOCKERFILE_START_CMD=$(dokku config:get $APP DOKKU_DOCKERFILE_START_CMD || true) + DOKKU_PROCFILE_START_CMD=$(get_cmd_from_procfile $APP $PROC_TYPE) + START_CMD=${DOKKU_DOCKERFILE_START_CMD:-$DOKKU_PROCFILE_START_CMD} fi if [[ "$PROC_TYPE" == "web" ]]; then diff --git a/plugins/common/functions b/plugins/common/functions index 0f1204fb460..b2bfa686a6b 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -186,6 +186,15 @@ is_number() { fi } +is_abs_path() { + local TEST_PATH=$1 + if [[ "$TEST_PATH" = /* ]]; then + return 0 + else + return 1 + fi +} + parse_args() { for arg in "$@"; do case "$arg" in @@ -211,6 +220,11 @@ copy_from_image() { verify_app_name $APP if verify_image "$IMAGE"; then + if ! is_abs_path $SRC_FILE; then + local WORKDIR=$(docker inspect -f '{{.Config.WorkingDir}}' $IMAGE) + [[ -z "$WORKDIR" ]] && WORKDIR=/app + SRC_FILE="$WORKDIR/$SRC_FILE" + fi CID=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $IMAGE bash) docker cp "$CID:$SRC_FILE" "$DST_DIR" docker rm -f "$CID" &> /dev/null @@ -264,6 +278,23 @@ get_app_running_container_ids() { echo "$APP_RUNNING_CONTAINER_IDS" } +get_cmd_from_procfile() { + local APP=$1; local PROC_TYPE=$2; local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE" + local name; local command; + verify_app_name $APP + + if [[ -f $DOKKU_PROCFILE ]]; then + while read line || [[ -n "$line" ]]; do + if [[ -z "$line" ]] || [[ "$line" == "#"* ]]; then + continue + fi + name="${line%%:*}" + command="${line#*:[[:space:]]}" + [[ "$name" == "$PROC_TYPE" ]] && echo "$command" && break + done < $DOKKU_PROCFILE + fi +} + is_deployed() { local APP="$1" if [[ -f "$DOKKU_ROOT/$APP/CONTAINER" ]] || [[ $(ls $DOKKU_ROOT/$APP/CONTAINER.* &> /dev/null; echo $?) -eq 0 ]]; then diff --git a/plugins/ps/functions b/plugins/ps/functions index 7321d6633bd..385ac3fe316 100755 --- a/plugins/ps/functions +++ b/plugins/ps/functions @@ -9,8 +9,31 @@ print_dokku_scale_file() { done < "$DOKKU_SCALE_FILE" } +extract_procfile() { + local APP="$1" + local IMAGE_TAG="$(get_running_image_tag $APP)" + local IMAGE="$(get_app_image_name $APP $IMAGE_TAG)" + local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE" + verify_app_name "$APP" + + copy_from_image "$IMAGE" "Procfile" "$DOKKU_PROCFILE" 2>/dev/null || true + if [[ -f "$DOKKU_PROCFILE" ]]; then + dokku_log_info1_quiet "App Procfile file found ($DOKKU_PROCFILE)" + else + dokku_log_info1_quiet "No Procfile found in app image" + fi +} + +remove_procfile() { + local APP="$1"; local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE" + if [[ -f "$DOKKU_PROCFILE" ]]; then + rm -f "$DOKKU_PROCFILE" + fi +} + generate_scale_file() { local APP="$1"; local IMAGE_TAG="$2"; local IMAGE=$(get_app_image_name $APP $IMAGE_TAG); local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE" + local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE" verify_app_name "$APP" copy_from_image "$IMAGE" "/app/DOKKU_SCALE" "$DOKKU_ROOT/$APP" 2>/dev/null || true @@ -20,18 +43,18 @@ generate_scale_file() { TMP_WORK_DIR=$(mktemp -d) trap 'rm -rf "$TMP_WORK_DIR" > /dev/null' RETURN - copy_from_image "$IMAGE" "/app/Procfile" "$TMP_WORK_DIR" 2>/dev/null || true - local PROCFILE="$TMP_WORK_DIR/Procfile" - if [[ ! -e "$PROCFILE" ]]; then - echo "web=1" >> $DOKKU_SCALE_FILE - else + if [[ -f $DOKKU_PROCFILE ]]; then while read -r line || [[ -n "$line" ]]; do - [[ "$line" =~ ^#.* ]] && continue + if [[ -z "$line" ]] || [[ "$line" == "#"* ]]; then + continue + fi NAME=${line%%:*} NUM_PROCS=0 [[ "$NAME" == "web" ]] && NUM_PROCS=1 [[ -n "$NAME" ]] && echo "$NAME=$NUM_PROCS" >> $DOKKU_SCALE_FILE - done < "$PROCFILE" + done < $DOKKU_PROCFILE + else + echo "web=1" >> $DOKKU_SCALE_FILE fi dokku_log_info1_quiet "New DOKKU_SCALE file generated" else diff --git a/plugins/ps/post-deploy b/plugins/ps/post-deploy index cb45e320b78..3a8338f4dac 100755 --- a/plugins/ps/post-deploy +++ b/plugins/ps/post-deploy @@ -5,4 +5,5 @@ source "$PLUGIN_AVAILABLE_PATH/ps/functions" APP="$1" +remove_procfile "$APP" dokku config:set --no-restart $APP DOKKU_APP_RESTORE=1 diff --git a/plugins/ps/pre-deploy b/plugins/ps/pre-deploy index 84f132d8919..44ec91a1f06 100755 --- a/plugins/ps/pre-deploy +++ b/plugins/ps/pre-deploy @@ -5,4 +5,5 @@ source "$PLUGIN_AVAILABLE_PATH/ps/functions" APP="$1" -generate_scale_file $APP +extract_procfile "$APP" +generate_scale_file "$APP" diff --git a/tests/apps/dockerfile-procfile/CHECKS b/tests/apps/dockerfile-procfile/CHECKS new file mode 100644 index 00000000000..f1d88bab389 --- /dev/null +++ b/tests/apps/dockerfile-procfile/CHECKS @@ -0,0 +1 @@ +/ nodejs/express diff --git a/tests/apps/dockerfile-procfile/Dockerfile b/tests/apps/dockerfile-procfile/Dockerfile new file mode 100644 index 00000000000..cd47658a53a --- /dev/null +++ b/tests/apps/dockerfile-procfile/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:trusty +ENV LC_ALL C +ENV DEBIAN_FRONTEND noninteractive +ENV DEBCONF_NONINTERACTIVE_SEEN true +EXPOSE 3001/udp +EXPOSE 3000/tcp +EXPOSE 3003 + +RUN apt-get install -y software-properties-common && add-apt-repository ppa:chris-lea/node.js && apt-get update +RUN apt-get install -y build-essential curl postgresql-client-9.3 nodejs git + +COPY . /app +WORKDIR /app +RUN npm install + +CMD npm start diff --git a/tests/apps/dockerfile-procfile/Procfile b/tests/apps/dockerfile-procfile/Procfile new file mode 100644 index 00000000000..9c49227a768 --- /dev/null +++ b/tests/apps/dockerfile-procfile/Procfile @@ -0,0 +1,8 @@ +############################### +# DEVELOPMENT # +############################### + +# Procfile for development using the new threaded worker (scheduler, twitter stream and delayed job) +cron: node worker.js +web: node web.js +worker: node worker.js diff --git a/tests/apps/dockerfile-procfile/check_deploy b/tests/apps/dockerfile-procfile/check_deploy new file mode 100755 index 00000000000..1334888e27c --- /dev/null +++ b/tests/apps/dockerfile-procfile/check_deploy @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +set -e; output="$(curl -s -S "$1")"; echo "$output"; test "$output" == "nodejs/express" diff --git a/tests/apps/dockerfile-procfile/package.json b/tests/apps/dockerfile-procfile/package.json new file mode 100644 index 00000000000..397b0ce84b0 --- /dev/null +++ b/tests/apps/dockerfile-procfile/package.json @@ -0,0 +1,13 @@ +{ + "name": "node-example", + "version": "0.0.1", + "dependencies": { + "express": "2.5.x" + }, + "engines": { + "node": "4.2.x" + }, + "scripts": { + "start": "false" + } +} diff --git a/tests/apps/dockerfile-procfile/web.js b/tests/apps/dockerfile-procfile/web.js new file mode 100644 index 00000000000..6b8f4d819d5 --- /dev/null +++ b/tests/apps/dockerfile-procfile/web.js @@ -0,0 +1,12 @@ +var express = require('express'); + +var app = express.createServer(express.logger()); + +app.get('/', function(request, response) { + response.send('nodejs/express'); +}); + +var port = process.env.PORT || 5000; +app.listen(port, function() { + console.log("Listening on " + port); +}); diff --git a/tests/apps/dockerfile-procfile/worker.js b/tests/apps/dockerfile-procfile/worker.js new file mode 100644 index 00000000000..12133530d9e --- /dev/null +++ b/tests/apps/dockerfile-procfile/worker.js @@ -0,0 +1,6 @@ +function worker() { + console.log('sleeping for 60 seconds'); + setTimeout(worker, 60 * 1000); +} + +worker(); diff --git a/tests/unit/10_ps-dockerfile.bats b/tests/unit/10_ps-dockerfile.bats index 75b9ba1d745..8d158e3d86c 100644 --- a/tests/unit/10_ps-dockerfile.bats +++ b/tests/unit/10_ps-dockerfile.bats @@ -116,3 +116,128 @@ teardown() { echo "status: "$status assert_success } + +@test "(ps) dockerfile with procfile" { + deploy_app dockerfile-procfile + run bash -c "dokku ps:stop $TEST_APP" + echo "output: "$output + echo "status: "$status + assert_success + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.*; do + run bash -c "docker ps -q --no-trunc | grep -q $(< $CID_FILE)" + echo "output: "$output + echo "status: "$status + assert_failure + done + + run bash -c "dokku ps:start $TEST_APP" + echo "output: "$output + echo "status: "$status + assert_success + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.*; do + run bash -c "docker ps -q --no-trunc | grep -q $(< $CID_FILE)" + echo "output: "$output + echo "status: "$status + assert_success + done + + run bash -c "dokku ps:restart $TEST_APP" + echo "output: "$output + echo "status: "$status + assert_success + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.*; do + run bash -c "docker ps -q --no-trunc | grep -q $(< $CID_FILE)" + echo "output: "$output + echo "status: "$status + assert_success + done + + run bash -c "dokku ps:rebuild $TEST_APP" + echo "output: "$output + echo "status: "$status + assert_success + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.*; do + run bash -c "docker ps -q --no-trunc | grep -q $(< $CID_FILE)" + echo "output: "$output + echo "status: "$status + assert_success + done +} + +@test "(ps:scale) dockerfile with procfile" { + run bash -c "dokku ps:scale $TEST_APP web=2 worker=2" + echo "output: "$output + echo "status: "$status + assert_success + + deploy_app dockerfile-procfile + for PROC_TYPE in web worker; do + run bash -c "docker ps --format '{{.ID}} {{.Command}}' --no-trunc" + echo "output: "$output + echo "status: "$status + assert_success + goodlines="" + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.$PROC_TYPE.*; do + cid=$(< $CID_FILE) + assert_output_contains "$cid" + goodlines+=$(echo "$output" | grep "$cid") + done + output="$goodlines" + assert_output_contains "node ${PROC_TYPE}.js" 2 + done + + run bash -c "dokku ps:scale $TEST_APP web=1 worker=1" + echo "output: "$output + echo "status: "$status + assert_success + for PROC_TYPE in web worker; do + run bash -c "docker ps --format '{{.ID}} {{.Command}}' --no-trunc" + echo "output: "$output + echo "status: "$status + assert_success + goodlines="" + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.$PROC_TYPE.*; do + cid=$(< $CID_FILE) + assert_output_contains "$cid" + goodlines+=$(echo "$output" | grep "$cid") + done + output="$goodlines" + assert_output_contains "node ${PROC_TYPE}.js" + done + + run bash -c "dokku ps:scale $TEST_APP web=0 worker=0" + echo "output: "$output + echo "status: "$status + assert_success + for PROC_TYPE in web worker; do + CIDS="" + shopt -s nullglob + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.$PROC_TYPE.*; do + CIDS+=$(< $CID_FILE) + CIDS+=" " + done + shopt -u nullglob + run bash -c "[[ -z \"$CIDS\" ]]" + echo "output: "$output + echo "status: "$status + assert_success + done +} + +@test "(ps:scale) procfile commands extraction" { + source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" + source "$PLUGIN_CORE_AVAILABLE_PATH/ps/functions" + cat < "$DOKKU_ROOT/$TEST_APP/DOKKU_PROCFILE" +web: node web.js +worker: node worker.js +EOF + run get_cmd_from_procfile "$TEST_APP" web + echo "output: "$output + echo "status: "$status + assert_output "node web.js" + + run get_cmd_from_procfile "$TEST_APP" worker + echo "output: "$output + echo "status: "$status + assert_output "node worker.js" +} diff --git a/tests/unit/30_ps-herokuish.bats b/tests/unit/30_ps-herokuish.bats index c55a3468ad7..cd2ccbfda72 100644 --- a/tests/unit/30_ps-herokuish.bats +++ b/tests/unit/30_ps-herokuish.bats @@ -114,7 +114,7 @@ teardown() { shopt -s nullglob for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.$PROC_TYPE.*; do CIDS+=$(< $CID_FILE) - CIDS+=" "1 + CIDS+=" " done shopt -u nullglob run bash -c "[[ -z \"$CIDS\" ]]" diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index fe04298a437..2bee6796c21 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -57,6 +57,17 @@ assert_output() { assert_equal "$expected" "$output" } +# ShellCheck doesn't know about $output from Bats +# shellcheck disable=SC2154 +assert_output_contains() { + local input="$output"; local expected="$1"; local count="${2:-1}"; local found=0 + until [ "${input/$expected/}" = "$input" ]; do + input="${input/$expected/}" + let found+=1 + done + assert_equal $count $found +} + # ShellCheck doesn't know about $lines from Bats # shellcheck disable=SC2154 assert_line() {