Using Docker for Development with Middleman | Ryan Bosinger

Ryan Bosinger

Web App Developer, Victoria BC

Using Docker for Development with Middleman

Over the last 3 years I’ve picked up and played with Docker a handful of times. The idea and promises behind what it offers always excite me but every time I experimented with it I would get to a certain point, begin to hit weirdness, call it quits and decide I’d be better off spending the time sharpening other skills.

Well, it’s that time of the year! I decided to spend a bit of time with Docker again (using Docker for Mac) and set up a little contained development environment for working with this site, which is built with Middleman.

Here’s How it Went

I initially updated Docker via Brew but when you take that direction it’s still on you to set-up a Linux VM in VirtualBox and connect through Docker Machine (I think). That’s a pain and Docker recommends Docker for Mac. So, I got that up and running.

Next I added a Dockerfile to the root directory of my Middleman project:

/Users/rbosinger/Projects/Personal/ryanbosinger_com_site/
▸ .git/
▸ build/
▸ source/
  .gitignore
  .ruby-version
  config.rb
  Dockerfile
  Gemfile
  Gemfile.lock
  README.md

My Dockerfile looks like this:

FROM ruby:2.3.4-onbuild
RUN bundle install
CMD ["middleman", "server"]
  1. We set a base image from DockerHub. This Ruby image is a hefty build and it’s certainly possible to build up your own, lighter, base image. I’m not going to bother with that yet; I just want to get rolling. One thing to note about this image is that it will assume your current directory is your Ruby app and copy your gemfile into the image for you. In other blog posts which may describe a more manual process you may see commands like ADD Gemfile and ADD Gemfile.lock. You can see the Dockerfile for this image here.

  2. We run bundle install to get things set-up.

  3. Run the command middleman server to get started right away.

That’s it! Now we can build an image from this Dockerfile like so:

sudo docker build -t rbosinger/ryanbosinger_com .

Here I’m using the -t option to give this image a name. This makes referencing it easier. Make sure to note the . at the end. This tells Docker that the Dockerfile is to be found in the current directory.

Running this command should give you output similar to this:

Step 1/3 : FROM ruby:2.3.4-onbuild
# Executing 4 build triggers...

Step 1/1 : COPY Gemfile /usr/src/app/
 ---> Using cache
Step 1/1 : COPY Gemfile.lock /usr/src/app/
 ---> Using cache
Step 1/1 : RUN bundle install
 ---> Using cache
Step 1/1 : COPY . /usr/src/app
 ---> e4cd6c0d2201
Removing intermediate container 17d6da3b0a45

Step 2/3 : RUN bundle install
 ---> Running in afc30abd0080
Using concurrent-ruby 1.0.5
Using i18n 0.7.0
...
Bundle complete! 14 Gemfile dependencies, 68 gems now installed.
 ---> 92cc6846f71b
Removing intermediate container afc30abd0080

Step 3/3 : CMD middleman server
 ---> Running in 2d73b17c9cf7
 ---> e0f2e0d76e37
Removing intermediate container 2d73b17c9cf7
Successfully built e0f2e0d76e37
Successfully tagged rbosinger/ryanbosinger_com:latest

Perfect. Now if we run docker images we should be able to see our newly created image:

REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
rbosinger/ryanbosinger_com   latest              e0f2e0d76e37        6 minutes ago       920MB

We have our image and now it’s time to run it in order to use it.

docker run -p 4567:4567 -p 1234:1234 -v /Users/rbosinger/Projects/Personal/ryanbosinger_com_site:/usr/src/app rbosinger/ryanbosinger_com

What’s going on here?

  1. We use the -p flag (for “port”) to specify port mappings between the container and our host. The Middleman server runs on port 4567 by default so we want to open that one. I’m also opening port 1234 for LiveReload (this is not the default port but I’ll explain further on).

  2. We use the -v flag (for “volume”) to mount our project directory into the Docker container. Without doing this we would have to rebuild our Docker image every time we changed a file. Note that I’m using my full local path to my Middleman project. I’m mapping that to /usr/src/app which was set-up by the Ruby image we inherited from.

  3. Finally, I specify the name of the image I want to run (rbosinger/ryanbosinger_com_site)

If all goes well you should see something like this:

== The Middleman is loading
== Blog Sources: /blog/{year}-{month}-{day}-{title}.html (:prefix + :sources)
== LiveReload accepting connections from ws://0.0.0.0:1234
== View your site at "http://localhost:4567", "http://127.0.0.1:4567"
== Inspect your site configuration at "http://localhost:4567/__middleman", "http://127.0.0.1:4567/__middleman"

We mapped port 4567 so pointing a browser to http://127.0.0.1:4567/ should load up the site. Nice.

Ok, why did you change the LiveReload port?

LiveReload, by default, uses port 35729. I tried many things but simply could not get my localhost to properly establish a websocket connection when using this port. In the end I made these changes and got it working. You may have different luck.

config.rb:

configure :development do
  activate :livereload, :host => '0.0.0.0', :port => '1234'
end

A Few Tips

Shutting down the server

It’s the same as always! Just hit CTRL-C to shut down the process. Run the docker run command again to bring it back up.

Checking things out

You can create another container from this image that runs a Bash shell in order to check things out.

~ docker run -i --rm -t rbosinger/ryanbosinger_com /bin/bash

root@75399157c8b8:/usr/src/app# ls
Dockerfile  Gemfile  Gemfile.lock  README.md  build  config.rb  source

If you list your containers you can see we now have two active containers built from our image:

CONTAINER ID        IMAGE                        COMMAND              CREATED              STATUS              
75399157c8b8        rbosinger/ryanbosinger_com   "/bin/bash"          6 seconds ago        Up 5 seconds                  
ccaa50968ee1        rbosinger/ryanbosinger_com   "middleman server"   About a minute ago   Up About a minute  

Here I am checking some versions within the container:

root@8fe4edd10b66:/usr/src/app# ruby -v
ruby 2.3.4p301 (2017-03-30 revision 58214) [x86_64-linux]

root@8fe4edd10b66:/usr/src/app# middleman version
Middleman 4.2.1

Pruning

When you run docker ps -a you’ll get a list of all containers, including ones that have already been exited/shutdown. You can clean this up by running:

docker system prune

Warning: I mention this assuming you’re playing with Docker on your own machine and don’t have any other important Docker-related stuff going on. Don’t just go running that anywhere.

Cheers,
Ryan