Web Development with Docker on Mac OS X

February 10th, 2016

Docker is taking off like a rocket-ship, and for good reason. It’s a great way to develop apps with consistency between development and production environments, that can be easily moved between hardware environments. This article will describe, as of February 10, 2016, how to set up a productive development environment with Docker on Mac OS X.

2017 Update: you can now install Docker for Mac, which runs natively without the need for a separate virtual machine, like docker-machine. If you have recent Mac hardware, use Docker for Mac instead. I think the article is still relevant, from the “Volumes” section down.

If you haven’t already, install Docker Toolbox. It has everything you need.

Much confusion arises with working on Docker on Mac OS X because of the three layers of operating system between you and the app:

  1. Mac OS X itself.
  2. A virtual machine (VM) running Linux inside VirtualBox, which you will start via docker-machine.
  3. Another Linux instance inside the Docker container, which is inside the VM.

Keeping these layers in mind is important because each layer has its own users, groups and file systems, which can result in permissions errors.

The current version of Docker Toolbox (Docker 1.10) will set up a VM named “default” that automatically mounts your Mac OS X “/Users” folder inside the (Linux) VM. If you already have an older installation of Boot2Docker that doesn’t automatically mount your Mac OS X “/Users” folder, make your life easier and create a new virtual machine.

The “/Users” folder will be automatically mounted in the VM, and the mount will be owned by a user named “docker” with a uid number of 1000:

$ docker-machine start default
Starting “default”…
(default) Check network to re-create if needed…
(default) Waiting for an IP…
Machine “default” was started.
Waiting for SSH to be available…
Detecting the provisioner…
Started machines may have new IP addresses. You may need to re-run the `docker-machine env` command.
$ eval "$(docker-machine env default)"
$ docker-machine ssh default 'df -h'
Filesystem Size Used Available Use% Mounted on
tmpfs 1.8G 129.4M 1.6G 7% /
tmpfs 1001.3M 500.0K 1000.8M 0% /dev/shm
/dev/sda1 18.2G 1.9G 15.3G 11% /mnt/sda1
cgroup 1001.3M 0 1001.3M 0% /sys/fs/cgroup
Users 464.8G 318.8G 146.0G 69% /Users
/dev/sda1 18.2G 1.9G 15.3G 11% /mnt/sda1/var/lib/docker/aufs
# /Users is mounted from OS X, inside the VM

$ docker-machine ssh default 'ls -l / | grep Users'
drwxr-xr-x 1 docker staff 340 Oct 25 17:05 Users
# the /Users mount is owned by "docker"
$ docker-machine ssh default 'id docker'
uid=1000(docker) gid=50(staff) groups=50(staff),100(docker)
# user "docker" has a uid number of 1000

If you want to access files within the /Users share from your Docker container, then you should do so as a user with uid 1000, otherwise you will likely have permissions errors.

Some Docker images c0me with a pre-made user for running the app in the image. That pre-made user may not have a uid of 1000, so it will not have permission to access the files in the /Users share. One way around this is to add an ENTRYPOINT script that changes the uid after the container starts up, or add a line to your Dockerfile to change the uid when the image is being built.

For example, I use the Phusion Passenger image, which comes with a user named “app” that has a uid of 9999. So I add this to my Dockerfile:

# Update app uid to match docker user uid
RUN /usr/sbin/usermod -u 1000 app

Note that usermod will automatically change the permissions of the files in the given user’s home directory, so that they are owned by the new uid.

I also specify an entrypoint script, which executes a few commands (more on that below) and then launches my app.

ADD entrypoint.sh /sbin/entrypoint.sh
RUN chmod 755 /sbin/entrypoint.sh
ENTRYPOINT ["/sbin/entrypoint.sh"]

I’m setting up Docker on OS X so that I can develop a Rails app that runs in the Linux environment, from the comfort of my Mac OS X desktop environment. As such, I want “live” updates between my OS X filesystem and the one inside the container. That is done using Volumes.

Volumes

To allow your app to read and/or write files from your Mac OS X filesystem, use what Docker calls, mounting a host directory as a data volume. In this case, the “host” is the “default” VM. It has the /Users folder from Mac OS X mounted at /Users.

An easy way to mount a host volume is to make use of the Docker Compose tool that was installed with Docker Toolbox. In the docker-compose.yml file, add a “volumes:” statement, in a like this:

web:
build: .
ports:
- '80:3000'
- '80:80'
volumes:
- /Users/brent/source/my_app:/home/app/my_app
links:
- postgres
postgres:
image: postgres:latest
ports:
- '5432:5432'

This tells Docker to mount the “/Users/brent/source/my_app” directory from the VirtualBox VM at a mount point inside the docker container, at “/home/app/my_app”.

Since the user “app” now has uid 1000, and the “docker” user in the VM that owns the “/Users” mount also has uid 1000, the app user will have read/write permissions on my source code folder.

To build your container as specified in the Dockerfile, and then run it with the parameters specified in the docker-compose.yml file, run:

$ docker-compose build
$ docker-compose up

Or just ‘docker-compose up’ to do them both in one step. Add a “-d” switch if you want to run it in the background.

Data Containers

Mounted host directories fullfill the need of programmers working on Mac OS X, but you can also use data containers to store data that you want to keep and possibly share among different app containers, or move around to different hardware environments. I use data containers to store my database data, and my ruby gems (so I don’t have to rebuild them every time I build a new app container).

I recommend reading Adrian Mouat’s excellent article, Understanding Volumes in Docker. It really helped me get my head around why they are useful and how they work. He links to this article, which is also very helpful.

I use them like this. For my database files, I create a “pgdata-dev” data container, seeded from the “postgres” image, by running:

$ docker run -v /var/lib/postgresql --name pgdata-dev postgres \
echo "Data-only container for postgres"

For my ruby gems, I seed a data container from the image that my ruby app uses (phusion/passenger-ruby), by running:

$ docker run -v /var/lib/gems --name rubygems “phusion/passenger-ruby22:0.9.18” echo “Data-only container for ruby gems”

Then I attach them to my application containers using docker’s “ — volumes-from” argument, via my docker-compose.yml file:

web:
build:
.
command: /sbin/entrypoint.sh
ports:
-
'80:3000'
- '3000:3000'
volumes:
-
/Users/brent/source/my_app:/home/app/my_app
volumes_from:
-
rubygems
links:
-
postgres
postgres:
image:
postgres:latest
volumes_from:
-
pgdata-dev
ports:
-
'5432:5432'

For Ruby on Rails, I build and persist ruby gems by putting these statements in my “entrypoint.sh” script (described above):

echo
echo "Installing Bundler..."
gem install bundler
echo
echo "Changing /var/lib/gems permissions to the app user..."
chown -R app:app /var/lib/gems
echo
echo "Running bundle install (as app user)..."
su - app -c "GEM_HOME=/var/lib/gems /home/app/workshops/bin/bundle install"

Now I can attach my “rubygems” data container to any rails app container, and don’t need to wait for the gems to download and/or compile every time.

Update: YMMV with running bundler as the app user to install gems in /var/lib/gems. Running bundler as root seems to work consistently, although I would much prefer running it as a regular user.