Continuous Deployment with Travis CI and Digital Ocean

Continuous integration and deployment is one of my favorite parts about working at Shutterstock. The team can push, run automated tests, and deploy many times in a single day. I've been wanting to try to build up something like this for my own projects and, by using a combination of Digital Ocean, Travis, and git hooks, I've created a basic workflow that allows me to do continuous integration for free.

First, some context. I'm running this application on Ubuntu 16 and PHP 7, though this flow should work for just about anything. It's a basic api/service with only a handful of endpoints so I have it running on the smallest droplet available.

The first thing to figure out is where the application is going to live on the remote server. I didn't want it to sit in the default /var/www/, so I changed some Apache configs to have it run from /opt/apps/{app-name}/. There are plenty ways to set this up - I chose to update the sites-available conf to route all traffic to the app directory.

Next is setting up the git repo on the remote that will receive the updates. This will be in a different directory - I went with /var/repo/{app-name}.git. Specifically, you'll want to create a bare repo that does nothing but move the committed files to your app directory.

  1. mkdir /var/repo

  2. mkdir /var/repo/{app-name}.git

  3. cd /var/repo/{app-name}.git

  4. git init --bare

Once the repo is setup it's time to create the hook. The hook will take all incoming git pushes and transfer the files to another location... The app directory. This is in hooks/post-receive

  1. #!/bin/sh

  2. git --work-tree=/opt/apps/{app-name} --git-dir=/var/repo/{app-name}.git checkout -f

Make sure the permissions is okay on this hook - "chmod +x hooks/post-receive" will do the trick.

So now we have the droplet ready to receive incoming git pushes and transfer the files over to the correct directory. It's time to move onto the Travis configuration. We'll need to have a deploy key in our repo for this, which is not a great thing to leave unencrypted in a code base. Here's some documentation on encrypting files for Travis.

  1. after_success:

  2. - bash bin/

Yup, that's all the configuration that .travis.yml needs. Things get more fun in the bash script.

  1. #!/usr/bin/env bash

  2. if [ "${TRAVIS_PULL_REQUEST}" == "false" ] && [ "${TRAVIS_BRANCH}" == "master" ]; then

  3. echo "Clears git information"

  4. rm -rf .git

  5. echo "Rebuilds dependencies"

  6. rm -rf vendor

  7. composer install --no-dev --no-interaction --ignore-platform-reqs

  8. echo "Writing custom gitignore for build"

  9. echo "# Build Ignores" > .gitignore

  10. echo "composer.phar" >> .gitignore

  11. echo "config.json" >> .gitignore

  12. echo "deploy_key.*" >> .gitignore

  13. echo "build/" >> .gitignore

  14. echo "codeclimate.json" >> .gitignore

  15. echo "coverage.xml" >> .gitignore

  16. echo "Sets up package for sending"

  17. git init

  18. git remote add deploy $DEPLOY_URI

  19. git config $DEPLOY_USER

  20. git config $DEPLOY_EMAIL

  21. git add --all .

  22. git commit -m "Deploy from Travis - build {$TRAVIS_BUILD_NUMBER}"

  23. echo "Sets up permissions"

  24. echo -e "Host\n\tStrictHostKeyChecking no" >> ~/.ssh/config

  25. openssl aes-256-cbc -K $encrypted_a9d53792e855_key -iv $encrypted_a9d53792e855_iv -in deploy_key.pem.enc -out deploy_key.pem -d

  26. eval "$(ssh-agent -s)"

  27. chmod 600 deploy_key.pem

  28. ssh-add deploy_key.pem

  29. echo "Sends build"

  30. git push -f deploy master

  31. fi

There's a lot here. First I added a check to make sure that this is only running on the master branch of the repo - I don't want test branches or pull requests to trigger a deploy. Then I nuke the git information in the directory, rebuild the dependencies to drop any dev-only packages, create a custom .gitignore (the .gitignore in the application is specific to development, not deploys), and then build a brand new repo. The new repo is fresh and clean and it needs a brand new user/email config, as well as endpoint. (Note: the endpoint is structured like ssh://{user}@{domain}:/var/repo/{app-name}.git.) Some of these environment variables are included with Travis, while others (DEPLOY_URI, DEPLOY_USER, DEPLOY_EMAIL) need to be added to the settings.

The permissions stuff is interesting - we need to trust the hostname or else the build will get stuck in a prompt. And then we need to decrypt the deploy key add it. This is all so that when the actual push occurs the build won't get stuck in a prompt for login. We want the build to be automated, not waiting for input.

One of the biggest pitfalls with this process is permissions. I monkeyed around a lot with the git repo, deploy keys, and app directory, trying to get the right permissions for getting the deploys to work and the app to continue running. If the git repo and app directory have the wrong configuration, the git push will succeed but the following redirection of files will fail, leaving a fun trail to debug. Now that it's actually working I want to tweak some things, move the commands around and maybe manipulate the final app structure, but am hesitant to touch something that appears to be working at the moment.