Be sure to take a look at Part 1: Building a PHP Command Line App with Docker before continuing this walkthrough.
At this point, we have already set up a PHP command-line application using Laravel's Artisan commands and Docker Compose. The rest of this tutorial will go through the process of setting up Codeship Pro to automate your continuous integration and deployments.
More specifically, here's what we're going to cover:
Using Jet to set up our environment and run our tests locally
Configuring Codeship Pro to run our tests whenever new code is pushed
Automatically deploying updates to our server after tests have passed
Continuous Integration
With our application and test suite running locally, the next thing we want to do is set up some kind of continuous integration system. You could set up your own servers to do this, but that's a fair amount of work, so I typically recommend a service like Codeship Pro.
Running tests locally with Jet
Before you run your tests on Codeship, I recommend setting up a local version of their CI platform, Jet. This will allow you to get things working faster; your configuration files might need some tweaking as your application evolves.
Once you've installed Jet, create two new files in the root directory of your project:
codeship-services.yml
- A variation of yourdocker-compose.yml
file specifically for Codeshipcodeship-steps.yml
- Instructions for the order and commands to run during continuous integration
The codeship-services.yml
file is almost identical to our docker-compose.yml
file:
version: "2.0" services: # PHP Application app: build: . links: - database encrypted_env_file: .env.encrypted command: cron -f # Database database: image: mariadb encrypted_env_file: .env.encrypted # Composer composer: image: composer/composer volumes: - ./:/app
And the codeship-steps.yml
file is pretty simple for this example because we want to run all our commands serially (one after the other). You can also run some steps in parallel if your application allows.
- type: serial steps: - service: composer command: install - service: app command: bash docker/codeship-run.sh
As you can see, the codeship-steps.yml
file calls a shell script that we haven't created yet. This script is going to make sure the app and database containers are up, the database migrations are run, and that the tests pass. It may sound like a lot, but it's actually a pretty simple file. Put this script in ./docker/codeship-run.sh
:
#!/usr/bin/env bash ## Ensure that the database is up and running function test_database { mysqladmin -h"$DB_HOST" -u"$DB_USERNAME" -p"$DB_PASSWORD" ping } count=0 until ( test_database ) do ((count++)) ## This will check up to 100 times. if [ ${count} -gt 100 ] then echo "Services didn't become ready in time" exit 1 fi ## And the script waits one second between each try sleep 1 done ## Create the database mysql -h"$DB_HOST" -u"$DB_USERNAME" -p"$DB_PASSWORD" -e 'CREATE DATABASE IF NOT EXISTS laravel' ## Run migrations php artisan migrate ## Run the test suite vendor/bin/phpunit
The first thing this script does is attempt to connect to the database. Codeship's software automatically brings up the app and database containers, but MySQL takes a few seconds to initialize, so we have to retry the test_database()
function until we get a success (or 100 tries). This is outlined in more detail in Codeship's Docker documentation.
Once the script is able to verify a database connection, it creates the default database (called laravel
). Then it runs the migrations to create the database table and the test suite via PHPUnit.
Finally, in order to test that this configuration works and the tests will pass, we can run everything with Jet:
$ jet steps
If everything is working, you should see a bunch of output as the containers are built and commands are run with a success message at the end:
{ContainerRunStdout=step_name:"serial_bash_docker/codeship-run.sh" service_name:"app"}: PHPUnit 5.7.19 by Sebastian Bergmann and contributors. {ContainerRunStdout=step_name:"serial_bash_docker/codeship-run.sh" service_name:"app"}: . 1 / 1 (100%) Time: 1.09 seconds, Memory: 12.00MB OK (1 test, 1 assertion) {StepFinished=step_name:"serial_bash_docker/codeship-run.sh" type:STEP_FINISHED_TYPE_SUCCESS} $
Connecting our repository to Codeship
If you haven't yet, add and commit your local code and push it to a new repository on GitHub or Bitbucket. Codeship automatically pulls code from your repository (public or private) each time you push changes, so now we just need to give Codeship the repository to watch.
Create a new project in Codeship, and connect it to your repository.
When prompted, choose "Codeship Pro" as your project type.
Now your project is linked to Codeship, and the next time you push your code, Codeship will attempt to build it and run your tests using the same codeship-steps.yml
and codeship-services.yml
files that you used locally. The only problem at this point is that you were using your local .env
file, and that should not be committed to your repository. Fortunately, there's an easy way to set environmental variables without compromising security.
Encrypting your environmental variables
Since it's best practice not to push our .env
file up to the continuous integration server, we need to come up with a way to securely pass variables up to Codeship. That's where an encrypted .env file comes in.
First, find your AES key on Codeship (it's in the General page of your project settings), and put it in a file at the root of your local project called codeship.aes
. Don't forget to add this file to your .gitignore
file, as it's an encryption key that should not be shared.
Next, update your codeship-services.yml
file to use an encrypted .env
file instead of the plain text one:
version: "2.0" services: # PHP Application app: build: . links: - database encrypted_env_file: .env.encrypted command: cron -f # Database database: image: mariadb encrypted_env_file: .env.encrypted # Composer composer: image: composer/composer volumes: - ./:/app
Now, use Jet to encrypt your .env
file as .env.encrypted
, commit that file to the repository, and push it to your remote repository:
$ jet encrypt .env .env.encrypted $ git add -A && git commit -am "Adding codeship config" $ git push origin
You should see Codeship running your build:
You can click into the build and see details about each step as well:
If everything above was done correctly, you should eventually get a successful build:
!Sign up for a free Codeship Account
Automated Deployments
A continuous integration service that tells us if our tests are passing is great, but in order to get the most value out of Codeship, it's very useful to have it automate our deployments as well. In order to do this, we'll need a few things:
The code repository manually configured and deployed on a server
An SSH key on the server that has permission to pull from our repository
A script on the server to update the code and restart the containers
With those things in place, we will be able to build a deployer container whose job is to SSH into the server and run the update script at the end of our build process.
I should say that this is just one way to deploy code using containers, and it may not be the best way for your production environment. Another option might be to use a container registry like Docker Hub to build your images and then update the containers directly from there. I've found that the best practices for Docker in production are still being established, so I favor this approach because it's simple and works for us.
Here's that process that I use in more detail:
Manually deploying code for the first time
This step will vary quite a bit depending on your hosting provider, but basically you will need a server provisioned that has Git, Docker, and Docker Compose installed. SSH into the server and:
Set up your
.env
file with a newAPP_KEY
and database password.Build and run the containers with Docker Compose:
docker-compose up -d --build
.
You should now be able to run docker ps
and see the same two containers running as when we set this project up locally for the first time.
Adding a script to update the server's code
Now on the local version of your code, we want to add a shell script that will pull updates from your repository and restart the containers:
#!/usr/bin/env bash ## Pull the latest code git pull origin master ## Rebuild the containers docker-compose up -d --build ## Run migrations docker exec dockerphpcliexample_app_1 php artisan migrate --force
This file should be called deploy.sh
and be put into the docker/
folder within your repository.
At this point, it's probably a good idea to make sure this file is on the server, so push your changes to the repository and pull them from the server. Test the script out by running $ bash docker/deploy.sh
from the server and making sure that your containers are still working.
Creating a deployer container
As I mentioned above, we now need a container to run this deploy script remotely from Codeship's CI server after the build and test process has completed. Create a new folder called deployer/
in your repository with a Dockerfile
, .env
file, and a shell script called execute.sh
:
FROM alpine:latest # Install openssh RUN apk update && apk add openssh # Prep for the ssh key RUN mkdir -p "$HOME/.ssh" RUN touch $HOME/.ssh/id_rsa RUN chmod 600 $HOME/.ssh/id_rsa # Add the shell script COPY execute.sh execute.sh CMD sh execute.sh
.env
USER=<SERVER_SSH_USERNAME> HOST=<SERVER_HOST> PRIVATE_SSH_KEY=<SSH_KEY (with linebreaks replaced with `\n`)>
See Codeship's article on SSH key authentication for more details.
execute.sh
#!/usr/bin/env bash echo -e $PRIVATE_SSH_KEY >> $HOME/.ssh/id_rsa ssh -t -oStrictHostKeyChecking=no $USER@$HOST "cd docker-php-cli-example && sh docker/deploy.sh"
This container will use the variables from the .env
file to SSH into the server and run the deploy script.
Making Codeship Pro run the deployer
In order to make Codeship aware of the deployer, we'll need to add it to the codeship-services.yml
file:
# Deployer deployer: build: ./deployer encrypted_env_file: deployer/.env.encrypted
And to the codeship-steps.yml
file:
- service: deployer command: sh execute.sh
We also want to encrypt the new deployer/.env
file so that we can commit it to the repository without exposing our server's SSH key. So just like we did with the main code repository, we're going to use Jet to encrypt the environment file:
$ jet encrypt deployer/.env deployer/.env.encrypted
Finally, push these updates to your GitHub repository and make sure Codeship successfully builds and deploys your code.
If you have any problems running this tutorial, feel free to add an issue to the GitHub repository I set up or find me on Twitter.