Adam Innes · Blog

Docker 1.0 for Web Developers: What Changes and What to Ask

· 7 min · docker, containers, devops, security

If you build web apps, you know the “works on my machine” routine. Your laptop has one version of Ruby, staging has another, and production has a system library nobody remembers installing. Docker has been pitching a way out of that for over a year, and last Monday, June 9, it tagged version 1.0.0. The 1.0.0 entry in the changelog is about as short as they come: production support. Docker’s announcement framed the number as a statement about quality, feature completeness, backward compatibility and API stability, and admitted that many organizations had already ignored its old “do not run in production” warnings. Docker Hub was announced the same day at DockerCon, a hosted service for sharing images with public and private repositories, automated builds and webhooks.

So here’s Docker from a web developer’s seat: what changes for local development and deploys, what a Dockerfile looks like in 1.0, and what to ask about security before trusting it with anything important.

Client, daemon and registry

The Understanding Docker page in the 1.0 docs describes a client and server design. The docker binary you type commands into is the client. It talks to the Docker daemon, which does the heavy lifting of building, running and distributing containers, on the same machine or a remote one. Registries hold images. Docker Hub is the public one, and you can also run your own behind your firewall.

Diagram of the docker client talking to the Docker daemon, which pulls from and pushes to a registry

If you’re on a Mac, there’s an extra hop. Docker depends on Linux kernel features, so the 1.0 install guide for OS X runs the daemon inside a lightweight virtual machine managed by Boot2Docker and points the Mac’s client at it with the DOCKER_HOST environment variable. On Linux the daemon runs directly on the host.

Images are stacks, containers add a writable layer

In Docker’s vocabulary an image is a read only template, something like Ubuntu with Ruby and your app installed. It’s made of layers combined with a union file system, and each instruction in a Dockerfile adds a new layer on top of the base image you start FROM. When you run a container, Docker puts a read write layer on top of the image and your process runs there. The glossary page on layers covers the copy on write part: change a file from a lower layer and it gets copied up into the writable layer, while the original stays exactly as it was.

Diagram of read only image layers from a Dockerfile with a writable container layer on top and a data volume beside them

That gives web work two nice properties. The image you tested doesn’t change, so it’s the thing you ship. And when you update your app, only the changed layers have to move around, which the docs credit for making image distribution fast. The catch is that a container’s writable layer isn’t a great home for data you care about, like uploads or database files. The 1.0 guide to managing data in containers covers data volumes, which bypass the union file system, persist until no containers use them, and aren’t included when you update an image.

The same page describes the trick that makes Docker pleasant for local development. With -v you can mount a directory from your machine into a container, and the guide suggests mounting your source code so you can see the app at work as you change it. The host path has to be absolute, adding :ro makes the mount read only, and you can’t do it from a Dockerfile, because a host directory is tied to one particular host.

A small Dockerfile in 1.0 terms

The Dockerfile reference at v1.0.0 documents thirteen instructions. COPY is the newest, arriving in 0.12.0 four days before 1.0 as a way to copy files from the build context without ADD’s habit of unpacking local tar archives.

Here’s a sketch for a tiny Sinatra app, modeled on the Ubuntu 14.04 example in the 1.0 user guide. I checked each instruction against the 1.0 reference, but treat it as a starting point, not something battle tested. The app is one file, app.rb:

require 'sinatra'

get '/' do
  'Hello from a container'
end

And the Dockerfile next to it:

FROM ubuntu:14.04
RUN apt-get -qq update
RUN apt-get -qqy install ruby ruby-dev
RUN gem install sinatra -v 1.4.5

ADD . /app
WORKDIR /app

USER nobody
EXPOSE 4567
CMD ["/usr/bin/ruby", "app.rb", "-o", "0.0.0.0"]

A few choices are worth explaining. The reference says that if FROM has no tag, latest is assumed, so naming 14.04 keeps a rebuild from quietly switching bases. The slow installs sit above ADD on purpose, because the reference says the first ADD invalidates the build cache for everything after it whenever the context’s contents have changed. Editing app.rb shouldn’t mean reinstalling Ruby. Each instruction also runs on its own, so RUN cd /app would have no effect on the next line, which is what WORKDIR is for. The reference prefers the JSON array form of CMD and says it needs the full path to the executable, hence /usr/bin/ruby.

The -o 0.0.0.0 is a Sinatra detail. In Sinatra 1.4.5 the default bind address is localhost in development mode, and I want the app listening on the container’s network interface, not just its loopback. EXPOSE tells Docker which port the container listens on, and publishing it to your machine happens at run time:

$ sudo docker build -t yourname/hello-sinatra .
$ sudo docker run -d -p 8080:4567 yourname/hello-sinatra

The 1.0 docs use sudo in their examples and say not to on OS X. Also remember that docker build sends the whole directory to the daemon as the build context. I didn’t find a way to exclude files in the 1.0 reference, so build from a directory that only holds what the image needs.

What it changes for deploys

You build an image once, push it to Docker Hub or your own registry, and pull and run that same image in CI, on staging and in production. The Docker Hub docs at 1.0 describe private repositories, automated builds that rebuild an image when you push to GitHub or Bitbucket, and webhooks that fire when an image is pushed, which is enough to hang a basic continuous deployment pipeline on. The unit you deploy stops being code plus a server setup checklist and becomes one image with its dependencies already inside.

The security questions worth asking

First, who can talk to the daemon? The Docker Security article in the 1.0 docs is blunt. The daemon currently requires root, and only trusted users should control it, because Docker lets you share a host directory with a container without limiting access. Someone who can start containers can mount the host’s root filesystem and change whatever they like. The Ubuntu install guide warns that the docker group is root equivalent, and the basics page says binding the daemon to a TCP port gives anyone who can reach that port full Docker access. For remote access the docs cover TLS with client certificates, and say to guard those keys like a root password.

One line in that article should catch a web developer’s eye. The API moved to a Unix socket in 0.5.2 because a TCP socket on 127.0.0.1 was prone to cross site scripting attacks when Docker ran directly on your machine. The article doesn’t spell out the attack, but my reading is that a page in your browser could send requests to an HTTP API on localhost. Think twice before exposing the API over TCP on the laptop you browse with.

Second, what can root inside a container do? The run reference says the default user in a container is root, and the security article concludes that containers are quite secure by default, especially if your processes run as non root users. That’s why the Dockerfile ends with USER nobody, and since the reference says ADD creates files owned by uid and gid 0, the app doesn’t even own its code. The default container template in the 1.0 source keeps just twelve Linux capabilities, such as CHOWN, NET_BIND_SERVICE and KILL, doesn’t allow all devices, and applies a docker-default AppArmor profile when AppArmor is enabled. Skip --privileged unless you really need it, since the run reference says it opens up all devices on the host.

Read that security article with some care, though. It’s adapted from an August 2013 blog post and shows its age: it says Docker uses lxc-start behind the scenes, while the 0.9.0 changelog made the libcontainer based native driver the default and the 1.0 command line reference lists native as the default exec driver. Its capability link also points at the 0.5.0 source. I’d read it for the threat model and check details against the code. It does lay out where Docker is headed, mapping a container’s root to an unprivileged host user and running the daemon without root, and suggests adding AppArmor, SELinux or GRSEC on top.

Third, do you trust the images you pull? The 1.0 user guide separates single word base images like ubuntu, which it says Docker Inc provides, from user images prefixed with a username, built and maintained by community members. Automated Builds help, since the Dockerfile is shown on the repository page and the image is built from the linked source, but I couldn’t find anything in the 1.0 docs about verifying a signature on a pulled image. So choose publishers carefully, pin tags, read the Dockerfile of anything you build FROM, and consider building your own base images for sensitive work. Also, docker login stores credentials in a .dockercfg file in your home directory, and ENV values persist into containers and show up in docker inspect, so keep secrets out of your Dockerfile.

Where that leaves us

Docker 1.0 changes how a web app travels from a laptop to a server. The image becomes the artifact you test and ship, the Dockerfile becomes a readable record of how it was built, and Docker Hub gives you somewhere to put it. Treat the daemon as the root level service it is, run your app as a non root user, and be as picky about base images as you are about the gems in your Gemfile.

← all posts