Jean-Philippe Boily / @jipiboily

Developer working remotely from Alma for Rainforest QA, often in pajamas.

August 13, 2013

Install Dokku + PostgreSQL with Docker for your Rails app (or whatever else, almost)

August 13, 2013 - JP - 

The plan with that blog post, which is more of a step-by-step thing where I won't explain a lot of things, but do it, is to have a VM with Dokku installed where you can push your app built with one of the supported language (by default: Ruby, Node.js, Java, Play!, Python, PHP, Clojure, Go, Dart) and run it with a PostgreSQL 9.1 server in it's own Docker container. I'll assume Rails for a few Rails specific steps, but you can skip those if you are not a Rails person.

What are Dokku and Docker?

I started to play with Dokku and Docker a few days ago. I heard about it before, but playing with it is just enlightening. Same for Dokku. Those are awesome pieces of software. Docker is a MAJOR game changer. What is it? Here is the official description:

"Docker is an open-source project to easily create lightweight, portable, self-sufficient containers from any application. The same container that a developer builds and tests on a laptop can run at scale, in production, on VMs, bare metal, OpenStack clusters, public clouds and more."

Roughly, Docker is built to have sandboxed containers running on your servers or local development VMs.

What about Dokku? "Docker powered mini-Heroku in less than 100 lines of Bash".

Ok, let's do it!

WARNING: this is not production proof at all, you would want to do a few things differently and add a bunch more stuff, mostly security and backup wise. Also, this won't scale horizontally at all. I did that for fun, not for production!

  1. Create your VM (I use Ubuntu 13.04 x64) and connect through SSH. I use Digital Ocean, use this referral link and get $10 of free credits!.
  2. On the server, set some environment variables for PostgreSQL (user, password and database name for your app's database plus the root password for PostgreSQL). Those will be used by later steps.
    export DATABASE_USER=username_here
    export DATABASE_PASSWORD=password_here
    export DATABASE_NAME=database_name_here
    export DATABASE_ROOT_PASSWORD=database_root_password
    
  3. Those commands will install Dokku, Docker and a PostgreSQL 9.1 container. It will also create a database and a database user, feel free to skip those last two if you don't care:

    # Install dokku
    wget -qO- https://raw.github.com/progrium/dokku/master/bootstrap.sh | sudo bash
    
    # Install pg (9.1)
    PGSQL=$(docker run -p 5432 -d synthomat/pgsql /usr/bin/start_pgsql.sh $DATABASE_ROOT_PASSWORD)
    DATABASE_IP=$(docker inspect $PGSQL | grep IPAddress | awk '{ print $2 }' | tr -d ',"')
    echo "Database IP: $DATABASE_IP"
    
    # Create .pgpass
    echo "$DATABASE_IP:5432:$DATABASE_NAME:$DATABASE_USER:$DATABASE_PASSWORD" > ~/.pgpass
    echo "$DATABASE_IP:5432:postgres:root:$DATABASE_ROOT_PASSWORD" >> ~/.pgpass
    chmod 600 ~/.pgpass
    
    # Install PostgreSQL client
    apt-get install postgresql-client-9.1 -y
    
    # Create DB, user and role
    createdb --username=root -h $DATABASE_IP $DATABASE_NAME
    createuser --login --username=root -h $DATABASE_IP $DATABASE_USER
    
  4. Some manual setup for PostgreSQL (first, please change the placeholders for real values and also remove square brackets). There might be command line equivalents for those, if so, please feel free to share in the comments.

    psql -h $DATABASE_IP -Uroot -d postgres
    grant all on DATABASE [DATABASE_NAME here] to [DATABASE_USER here];
    ALTER USER [DATABASE_USER here] WITH PASSWORD '[DATABASE_PASSWORD here]';
    
  5. Set hostname by editing /home/git/VHOST. Set it to the root domain you will use. In my case, it could be 'jipiboily.com'. The app would be created under that domain (ie: app.jipiboily.com). For more information on that one: https://github.com/progrium/dokku/#configuring.
  6. On your local machine, in your project's directory (it must be a git repo. Also, change the placeholders and remove square brackets):
    cat ~/.ssh/id_rsa.pub | ssh root@[FULL HOSTNAME] "gitreceive upload-key progrium"
    git remote add dokku git@[FULL HOSTNAME]:[PROJECT NAME]
    git push dokku master
    
  7. After the push, on the server, create a ENV file in your project's directory (/home/git/[PROJECT NAME]/ENV):
    # /home/git/[PROJECT NAME]/ENV
    export DATABASE_USER=database_user
    export DATABASE_PASSWORD=password_here
    export DATABASE_NAME=database_name_here
    export DATABASE_IP=content of $DATABASE_IP
    export DATABASE_URL=postgres://$DATABASE_USER:$DATABASE_PASSWORD@$DATABASE_IP:5432/$DATABASE_NAME
    export RAILS_ENV=production
    
  8. Re-push (yeah, you apparently need to do that for now so that env vars will be used next time. It might work without re-pushing if you were quick enough to create the ENV file BEFORE the end of the initial git push).
  9. For Rails projects, to run migrations, for now, it seems a bit of a pain. Here is how I do it, but I will look at a better solution at some point for sure:
    export BUNDLE_PATH=/app/vendor/ruby-2.0.0/bin/bundle
    export DOKKU_IMAGE_ID=$(docker images|grep -i app/ | awk '{ print $3 }') # this line assume there is a single app, change "app/" by "app/my-app-name-here" if there is more than one
    export DOKKU_ID=$(docker ps|grep -i app/ | awk '{ print $1 }')
    export DOKKU_IP=$(docker inspect $DOKKU_ID | grep IPAddress | awk '{ print $2 }' | tr -d ',"')
    docker run -e PATH="/app/vendor/ruby-2.0.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" -e DATABASE_URL=postgres://$DATABASE_USER:$DATABASE_PASSWORD@$DATABASE_IP:5432/$DATABASE_NAME -e RAILS_ENV=production $DOKKU_IMAGE_ID bash -c "gem install bundler && cd /app && $BUNDLE_PATH install && $BUNDLE_PATH exec rake db:migrate"
    
    9.1. Want to run a Rails console? Here it is (it will start a new container with equivalent setup):
    export BUNDLE_PATH=/app/vendor/ruby-2.0.0/bin/bundle
    export DOKKU_IMAGE_ID=$(docker images|grep -i app/| awk '{ print $3 }') # this line assume there is a single app, change "app/" by "app/my-app-name-here" if there is more than one
    export DOKKU_ID=$(docker ps|grep -i app/ | awk '{ print $1 }')
    export DOKKU_IP=$(docker inspect $DOKKU_ID | grep IPAddress | awk '{ print $2 }' | tr -d ',"')
    docker run -i -t -e PATH="/app/vendor/ruby-2.0.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" -e DATABASE_URL=postgres://$DATABASE_USER:$DATABASE_PASSWORD@$DATABASE_IP:5432/$DATABASE_NAME -e RAILS_ENV=production $DOKKU_IMAGE_ID bash -c "gem install bundler && cd /app && $BUNDLE_PATH install && $BUNDLE_PATH exec rails console"
    
  10. Backups (not robust or production ready AT ALL) The steps will include a S3 client, create a basic script and then a cron job. Nothing production ready, but if you want to play with something you still want to have backups for, here is how you could do it. Really, really simple and NOT production proof AT ALL. Understood? ;)
    apt-get install s3cmd -y
    s3cmd --configure
    
    Follow the steps (giving access and secret keys)
    touch ~/pg-backup.sh
    chmod +x ~/pg-backup.sh
    vi ~/pg-backup.sh
    
    Content of the pg-backup.sh file (please change the project directory on line 3 and S3 bucket name on last line):
    #!/bin/bash
    # /root/pg-backup.sh
    source /home/git/[YOUR PROJECT NAME HERE]/ENV
    DATE=`date +%Y-%m-%d`
    FILE_NAME=$DATABASE_NAME.$DATE.pg.dump
    pg_dump -Fc --no-acl --no-owner --username=$DATABASE_USER -h $DATABASE_IP $DATABASE_NAME>$FILE_NAME
    s3cmd put $FILE_NAME s3://BUCKET-NAME/$FILE_NAME
    
    Create the cron job by using "crontab -e" and add a line that could look like this:
    0 2 * * * /root/pg-backup.sh
    

The end.

You can now visit http://[PROJECT NAME].[HOSTNAME] and be happy! :D

You should now have a running app, a database container and basic backups for it. This was really rough but should be enough to get you started.

Bonus

Docker and Dokku tips

  • docker ps -a to list all containers, even stopped ones.
  • docker logs ID_HERE to see the logs of a specific controller.
  • you need to redeploy if you want new ENV vars to be taken into accounts (at least for now).

You loved that? Tell the world!