Heroku on Docker

I am sure you have heard of Docker, but have you ever actually deployed a real app on it? How would you even start to move Heroku’s 4+ million apps into Docker Containers?

Not many people have even tried. Building an app on Docker can be incredibly hard and frustrating. Not at all like using Heroku where everything is taken care of for you. With Docker, you have to learn about Dockerfiles and then try to get one that works just right with your code.

If you are lazy (like me) and want to just try out Docker with no fuss, I will guide you through the whole process from start to finish. In the end, we will have generic containers we can use with any Docker Developer Tool like CoreOS or Fig. We will create:

  • Easy: a simple Node application container
  • Medium: a Rails application container
  • Hard: a HHVM WordPress application container

All without learning anything about Linux Containers or Dockerfiles.

Installing Docker

If you are on a Mac, the install process has been greatly improved. You can now get Docker running in just seconds:

$ brew install boot2docker # if you are not on a Mac, go to https://www.docker.io/gettingstarted/
$ boot2docker init
$ boot2docker up
$ echo 'export DOCKER_HOST=tcp://localhost:4243' >> .bash_profile
$ source .bash_profile
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

How to Turn Any Heroku App Into a Docker Container

You can use github.com/CenturyLinkLabs/building (like progrium/buildstep but without tarfiles and with extra goodies) to turn any Heroku-compatible app into a Docker Container:

$ sudo gem install building
$ cd path/to/app
$ building username/containername
$ docker push username/containername # optional: this will allow you to share your app container with anyone in the world via docker pull username/containername. Make sure to make it a private repo if it has your private code in it

Yes, that’s it. Everything else is handled for you. Code discovery: done. Dependencies: done. Startup script: done. Heroku on Docker: done.

Converting Your Node App to a Container

This works with any technology: Ruby, Node, Java, Play, Python, PHP, Clojure, Go, Dart and more. Don’t believe me? Let’s try a simple Node application.

$ cat server.js
var PORT = process.env.PORT || 8080;
var express = require("express");

var app = express();
app.use(app.router);
app.use(express.static(__dirname + "/public"));

app.get("/", function(req, res){
    res.send("Hello, World!");
});

app.listen(PORT);

$ cat package.json 
{
    "name": "dokku-demo-application",
    "version": "1.0.0",

    "private": true,

    "engines": {
        "node": ">=0.10.0",
        "npm": ">=1.3"
    },

    "dependencies": {
        "express": "~3.0"
    }
}

$ building myuser/container-name
Uploading context 5.632 kB
Uploading context 
Step 0 : FROM ctlc/buildstep:ubuntu13.10
 ---> a5432f93c775
Step 1 : ADD . /app
 ---> fa81359c2c88
Step 2 : RUN /build/builder
 ---> Running in 80ee61eee492
       Node.js app detected

       PRO TIP: Avoid using semver ranges starting with '>' in engines.node
       See https://devcenter.heroku.com/articles/nodejs-support

-----> Requested node range:  >=0.10.0
-----> Resolved node version: 0.10.26
-----> Downloading and installing node
-----> Writing a custom .npmrc to circumvent npm bugs
-----> Installing dependencies
       npm http GET https://registry.npmjs.org/express
       npm http 200 https://registry.npmjs.org/express
...
-----> Caching node_modules directory for future builds
-----> Cleaning up node-gyp and npm artifacts
-----> No Procfile found; Adding npm start to new Procfile
-----> Building runtime environment
-----> Discovering process types
       Procfile declares types -> web
 ---> 163e52f5f836
Step 3 : CMD /start web
 ---> Running in 3ce46a2994d7
 ---> 2b6ce52c1812
Successfully built 2b6ce52c1812
Removing intermediate container 09487f499f72
Removing intermediate container 80ee61eee492
Removing intermediate container 3ce46a2994d7

To run your app, try something like this:

	docker run -d -p 8080 -e "PORT=8080" myuser/container-name:latest

$ docker run -d -p 8080 -e "PORT=8080" myuser/container-name:latest
000a16dc433be0f0d32b2fa80d7fc842a6cb05369776cdc44e2906da3d465468
$ docker ps
CONTAINER ID        IMAGE                          COMMAND                CREATED             STATUS              PORTS                     NAMES
000a16dc433b        myuser/container-name:latest   /bin/sh -c /start we   12 seconds ago      Up 11 seconds       0.0.0.0:49193->8080/tcp   focused_mccarthy   
$ boot2docker down
$ VBoxManage modifyvm "boot2docker-vm" --natpf1 "tcp-port49193,tcp,,49193,,49193";
$ boot2docker up
$ curl 0.0.0.0:49193
Hello, World!

Converting Your Rails App to a Container

Ok, that was cool, but how about a Rails app? Can you do a Rails app?

$ rails new myrailsapp
$ cd myrailsapp
$ building myuser/rails-container-name
Uploading context  85.5 kB
Uploading context 
Step 0 : FROM ctlc/buildstep:ubuntu13.10
 ---> a5432f93c775
Step 1 : ADD . /app
 ---> 770f66d24f09
Step 2 : RUN /build/builder
 ---> Running in 0cd713d0d6e8
       Ruby app detected
-----> Compiling Ruby/Rails
-----> Using Ruby version: ruby-2.0.0
-----> Installing dependencies using 1.5.2
-----> Writing config/database.yml to read from DATABASE_URL
-----> Preparing app for Rails asset pipeline
-----> Discovering process types
       Default process types for Ruby -> rake, console, web, worker
 ---> a78e12a093ed
Step 3 : CMD /start web
 ---> Running in ddb1d8b13f8d
 ---> 9e2ea01bdbbf
Successfully built 9e2ea01bdbbf
Removing intermediate container 4089b1d484f2
Removing intermediate container 0cd713d0d6e8
Removing intermediate container ddb1d8b13f8d

To run your app, try something like this:

	docker run -d -p 8080 -e "PORT=8080" myuser/rails-container-name:latest

$ docker run -d -p 8080 -e "PORT=8080" myuser/rails-container-name:latest
c4a05c8bfbb67f43ea53d661d14bd0e018a54a4c9389572105345f82326cf39f

Converting Your WordPress to a Container Running HHVM

Ok, that was cool, but how about WordPress running HHVM?

$ curl http://wordpress.org/latest.tar.gz | tar xvz
$ cd wordpress
$ building myuser/wordpress-container-name
Uploading context 17.67 MB
Uploading context 
Step 0 : FROM ctlc/buildstep:ubuntu13.10
 ---> a5432f93c775
Step 1 : ADD . /app
 ---> 12f02e9a07ca
Step 2 : RUN /build/builder
 ---> Running in 592e7f8d4778
       PHP (classic) app detected
-----> Bundling NGINX 1.4.4
-----> Bundling PHP 5.5.10
-----> Bundling extensions
       apcu
       phpredis
       mongo
-----> Setting up default configuration
-----> Vendoring binaries into slug
-----> Discovering process types
       Default process types for PHP (classic) -> web
 ---> 935abb4b0b50
Step 3 : CMD /start web
 ---> Running in 90aa3b71513a
 ---> 2d85ad2102c8
Successfully built 2d85ad2102c8
Removing intermediate container ffff9fab4bf2
Removing intermediate container 592e7f8d4778
Removing intermediate container 90aa3b71513a

To run your app, try something like this:

	docker run -d -p 8080 -e "PORT=8080" myuser/wordpress-container-name:latest

$ docker run -d -p 8080 -e "PORT=8080" myuser/wordpress-container-name:latest
21fe085ce024e58d248fb64c1c8b9f914f3c09acc039d734897ea0bf2f36f3b8

Wait, that’s not HHVM. That’s nginx. No fair, I cheated. Now to do it right:

$ building -b https://github.com/hhvm/heroku-buildpack-hhvm.git \
    -f ctlc/buildstep:ubuntu12.04 \
    myuser/wordpress-container-name hhvm
Uploading context 17.67 MB
Uploading context 
Step 0 : FROM ctlc/buildstep:ubuntu12.04
 ---> aa021eb57a6f
Step 1 : RUN git clone --depth 1 https://github.com/hhvm/heroku-buildpack-hhvm.git /build/buildpacks/heroku-buildpack-hhvm
 ---> Using cache
 ---> 16cf1eb81d80
Step 2 : RUN echo https://github.com/hhvm/heroku-buildpack-hhvm.git >> /build/buildpacks.txt
 ---> Using cache
 ---> f6f51ceb615f
Step 3 : ADD . /app
 ---> ff188b8dfe49
Step 4 : RUN /build/builder
 ---> Running in 7ebccf86608d
       PHP (HHVM) app detected
-----> Downloading HHVM from http://dl.hhvm.com/heroku/hhvm-nightly_2014.03.20~lucid.tgz
-----> Sourcing config.hdf
-----> Custom config.hdf not found, applying default
-----> Installing dependencies using Composer
-----> composer.json not found
-----> Discovering process types
       Default process types for PHP (HHVM) -> web
 ---> 94286be3d51d
Step 5 : CMD /start web
 ---> Running in 01481e9e815e
 ---> c272d7ed4fc0
Successfully built c272d7ed4fc0
Removing intermediate container d2bd8e443091
Removing intermediate container 7ebccf86608d
Removing intermediate container 01481e9e815e

To run your app, try something like this:

	docker run -d -p 8080 -e "PORT=8080" myuser/wordpress-container-name:hhvm

$ docker run -d -p 8080 -e "PORT=8080" myuser/wordpress-container-name:hhvm
ee674c7428752c8e4a20e21fc8a63485899be083287a9db47b6b9e16b97ae4ff

But With Heroku, I Can Scale On the Command Line

With building you can too. There is a -o flag that lets you set a fig.yml output file which you can pass to fig.

$ brew install python # if you are on a Mac
$ sudo pip install -U fig
$ building -o fig.yml myuser/wordpress-container-name:hhvm
$ fig up -d
Creating myapp_web_1...
$ fig scale web=3
Starting myapp_web_2...
Starting myapp_web_3...
$ fig ps
Name        Command     State        Ports      
--------------------------------------------------
myapp_web_3   /start web   Up      49192->8080/tcp 
myapp_web_2   /start web   Up      49191->8080/tcp 
myapp_web_1   /start web   Up      49190->8080/tcp 

Notice that when you combine building with fig, you never need to manually run a docker command again. Ahhh, abstraction.

Conclusion

One of the most powerful parts of Docker is the fact that you can modify every byte of the underlying “slug” as Heroku calls it. You can put any binaries in any locations. You can build your own Docker images from scratch. But that power comes at a price: ease of use. Tools like building and fig set out to bridge the gap and let everyday developers take advantage of cool new technology without having to get a PhD in DevOps.

  • Thomas Eichberger

    Very interesting. Thx for the info.

  • http://www.philwhln.com philwhln

    Thanks for a tutorial! btw, I needed to run “boot2docker init” before “boot2docker up”, otherwise I get “boot2docker-vm does not exist.”

  • Solomon Hykes

    This is fun but I wouldn’t use it for serious workflows – or any tool which attempts to wrap Docker just to add an extra feature. This breaks every tool which relies on a native docker endpoint for integration.

    Really, I have to call “builder” instead of “docker build”? Why not help me convert the buildpack to a native docker image, so that I can “docker build” it myself? That’s exactly why we created the ONBUILD instructions. Such a tool would be easy to write. Lucas, I’ll help you if you want. But please, please stop it with the cheap single-purpose wrappers. Your blog has a lot of readers, and you’re positioning yourself as a provider of Docker best practices. Let’s give them some real best practices.

    • CenturyLink Labs

      Solomon, that’s exactly what the “building” tools does. All it does is converts apps into native docker images. The “building” tool runs “docker build” for you, but it could just as easily just print out the “docker build” command for you to use. I think you misunderstood this post. :)

    • CenturyLink Labs

      In fact, one of the outputs of the “building” tool is a Dockerfile in your app which you can use to “docker build” yourself