Docker tar pipe

This blog post is now out of date. The docker tar-pipe is now executed from micro-services implemented in Ruby.
For example, see runner.



I've been working on re-architecting cyber-dojo so the web-server (written in Rails) runs in a Docker image...
  • the server receives its source files from the browser
  • it saves them to a temporary folder
  • it back-ticks a shell file which
  • ...puts the source files into a docker container
  • ...runs the source files (by executing cyber-dojo.sh) as user=nobody
  • ...limits execution to 10 seconds


My shell file started like this:
#!/bin/sh SRC_DIR=$1 # where source files are IMAGE=$2 # the image to run them in MAX_SECS=$3 # how long they've got to complete TAR_FILE=`mktemp`.tgz # source files are tarred into this SANDBOX=/sandbox # where tar is untarred to inside container # - - - - - - - - - - - - - - - - - - - # 1. Create the tar file cd ${SRC_DIR} tar -zcf ${TAR_FILE} . # - - - - - - - - - - - - - - - - - - - # 2. Start the container CID=$(sudo docker run --detach \ --interactive \ --net=none \ --user=nobody \ ${IMAGE} sh) # - - - - - - - - - - - - - - - - - - - # 3. Pipe the source files into the container cat ${TAR_FILE} \ | sudo docker exec --interactive \ --user=root \ ${CID} \ sh -c "mkdir ${SANDBOX} \ && tar zxf - -C ${SANDBOX} \ && chown -R nobody ${SANDBOX}" # - - - - - - - - - - - - - - - - - - - # 4. After max_seconds, remove the container (sleep ${MAX_SECS} && sudo docker rm --force ${CID}) & # - - - - - - - - - - - - - - - - - - - # 5. Run cyber-dojo.sh in the container sudo docker exec --user=nobody \ ${CID} \ sh -c "cd ${SANDBOX} && ./cyber-dojo.sh 2>&1" # - - - - - - - - - - - - - - - - - - - # 6. If the container isn't running, the sleep woke and removed it RUNNING=$(sudo docker inspect --format="{{ .State.Running }}" ${CID}) if [ "${RUNNING}" != "true" ]; then exit 137 # (128=timed-out) + (9=killed) else exit 0 fi


Things to note:
  • The container is started in detached mode. This is so I can get its CID and setup the backgrounded sleep task (4) before running cyber-dojo.sh (5)
  • I use [sudo docker] because I do not put the current user into the docker group. Instead I sudo the current user to run the docker binary without a password.
  • The first [docker exec] user is root but this is root inside the CID container not root where the shell file is being run.
  • I can pipe STDIN from the shell into the container
  • The sleep task (4) kills the container if it runs out of time and step (6) detects this.


I realized I could avoid creating the (physical) tar file completely by using a 'proper' tar pipe:
#!/bin/sh ... (cd ${SRC_DIR} && tar -zcf - .) \ | sudo docker exec --interactive \ --user=root \ $CID \ sh -c "mkdir ${SANDBOX} \ && tar -zxf - -C ${SANDBOX} \ && chown -R nobody ${SANDBOX}" ...


  • [tar -zcf] means create a compressed tar file
  • [-] means don't write to a named file but to STDOUT
  • [.] means tar the current directory
  • which is why there's a preceding cd
At the other end of the pipe...
  • [tar -zxf] means extract files from the compressed tar file
  • [-] means don't read from a named file but from STDIN
  • [-C ${SANDBOX}] means save the extracted files to the ${SANDBOX} directory


Then I realized I could combine the two [docker exec]s into one and drop the chown...
... # - - - - - - - - - - - - - - - - - - - # 1. Start the container CID=$(sudo docker run \ --detach \ --interactive \ --net=none \ --user=nobody \ ${IMAGE} sh) # - - - - - - - - - - - - - - - - - - - # 2. After max_seconds, remove the container (sleep ${MAX_SECS} && sudo docker rm --force ${CID}) & # - - - - - - - - - - - - - - - - - - - # 3. Tar pipe the source files into the container and run (cd ${SRC_DIR} && tar -zcf - .) \ | sudo docker exec --interactive \ --user=nobody \ ${CID} \ sh -c "mkdir ${SANDBOX} \ && cd ${SANDBOX} \ && tar -zxf - -C . \ && ./cyber-dojo.sh" # - - - - - - - - - - - - - - - - - - - # 4. If the container isn't running, the sleep woke and removed it RUNNING=$(sudo docker inspect --format="{{ .State.Running }}" ${CID}) if [ "${RUNNING}" != "true" ]; then exit 137 # (128=timed-out) + (9=killed) else exit 0 fi


This worked but the backgrounded sleep created a zombie process.
That's for another blog.