I have a PHP project on GitHub for which I would like to build versioned Docker images. Kinda like CoreOS Container Linux does with their alpha, beta and stable release channels.
For development I follow the Git Flow principle and use the feature -> develop -> master branching.
I have hooked up Travis for automated PHPUnit testing and added CircleCI today as I found a Rakefile which could automatically generate a version. Which is used as a tag.
My current .travis.yml:
---
services:
- docker
sudo: required
env:
global:
- ...
addons:
jwt:
secure: ...
cache:
directories:
- /home/travis/docker/
before_install:
- "docker --version"
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then echo "ENV GIT_SHA ${TRAVIS_COMMIT::8}" >> Dockerfile; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]] && [[ -f ${DOCKER_CACHE_FILE} ]]; then gunzip -c ${DOCKER_CACHE_FILE} | docker load; fi'
before_script:
- "env > .env"
install:
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then docker build -t ${DOCKER_REPOSITORY}:${TRAVIS_COMMIT::8} --pull=true .; fi'
language: php
notifications:
slack:
secure: ...
php:
- "5.6"
script:
- "phpunit --bootstrap tests/bootstrap.php --testdox tests --coverage-text"
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then mkdir -p $(dirname ${DOCKER_CACHE_FILE}) ; docker save $(docker history -q ${DOCKER_REPOSITORY}:${TRAVIS_COMMIT::8} | grep -v "<missing>") | gzip > ${DOCKER_CACHE_FILE}; fi'
after_success:
- 'if [[ $TRAVIS_PHP_VERSION == "5.6" ]] && [[ $TRAVIS_BRANCH == "develop" ]] && [[ $TRAVIS_PULL_REQUEST == "false" ]]; then sh generate-api.sh; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD}; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then docker tag ${DOCKER_REPOSITORY}:${TRAVIS_COMMIT::8} ${DOCKER_REPOSITORY}:${TRAVIS_COMMIT::8}; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then docker tag ${DOCKER_REPOSITORY}:${TRAVIS_COMMIT::8} ${DOCKER_REPOSITORY}:travis-${TRAVIS_BUILD_NUMBER}; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then docker tag ${DOCKER_REPOSITORY}:${TRAVIS_COMMIT::8} ${AWS_ACCOUNT_NUMBER}.dkr.ecr.eu-west-1.amazonaws.com/${ECR_REPOSITORY}:${TRAVIS_COMMIT::8}; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then docker push ${DOCKER_REPOSITORY}; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then pip install --user awscli; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then export PATH=$PATH:$HOME/.local/bin; fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then eval $(aws ecr get-login --region eu-west-1); fi'
- 'if [[ ${TRAVIS_BRANCH} == "master" ]] && [[ ${TRAVIS_PULL_REQUEST} == "false" ]]; then docker push ${AWS_ACCOUNT_NUMBER}.dkr.ecr.eu-west-1.amazonaws.com/${ECR_REPOSITORY}:${TRAVIS_COMMIT::8}; fi'
And my current circle.yml:
---
machine:
services:
- docker
php:
version: 5.6.22
dependencies:
override:
- docker info
- gem install httparty
- rake build
test:
override:
- mkdir -p $CIRCLE_TEST_REPORTS/phpunit
- phpunit --bootstrap tests/bootstrap.php --log-junit $CIRCLE_TEST_REPORTS/phpunit/junit.xml tests
- docker run -d -p 9000:9000 storecore/php:latest; sleep 10
- nc -z -w5 localhost 9000
And the used Rakefile:
require 'rake'
require 'httparty'
require 'json'
# Read the base version from VERSION file.
def version
file = File.readlines('./version.php')
v = file[1].scan(/'([^']*)'/)
v[1].join(",")
end
# The name of the container
def container_name
'php'
end
# The username the container is pushed to on DockerHub
def username
'storecore'
end
# Get the latest version for the given base version provided by #version
def hub_version
base = version
taginfo = JSON.parse(HTTParty.get("https://hub.docker.com/v2/repositories/#{username}/#{container_name}/tags/").body)['results']
return { base: base, build: nil } if taginfo.nil?
tags = []
taginfo.each do |tag|
tags << tag['name']
end
current_base = tags.grep(/#{base}/)
return { base: base, build: nil } if current_base.empty?
build = current_base.sort { |x, y|
a = x.split('.')[base.split('.').count].to_i
b = y.split('.')[base.split('.').count].to_i
a <=> b
}.last.split('.').last.to_i
{ base: base, build: build }
end
# return current hub version for the current base
def latest_hub_version
latest = hub_version
"#{latest[:base]}.#{latest[:build]}"
end
# return the next version for the current base
def next_version
latest = hub_version
base = version
build = latest[:build] || -1
build += 1
"#{base}.#{build}"
end
task :install_deps do
sh 'gem install bundler'
sh 'bundle install'
end
desc 'login into Docker Hub'
task :login do
sh "docker login -u #{ENV['DOCKER_USER']} -p #{ENV['DOCKER_PASS']}"
end
desc 'tags latest as next_version'
task :tag => :login do
sh "docker tag #{username}/#{container_name}:latest #{username}/#{container_name}:#{next_version}"
end
desc 'pushes the next_version and latest to docker hub'
task :push => :tag do
sh "docker push #{username}/#{container_name}:#{next_version}"
sh "docker push #{username}/#{container_name}:latest"
end
desc 'builds as latest'
task :build => :install_deps do
sh "docker build --rm=false -t #{username}/#{container_name}:latest ."
end
task default: [:build, :push]
But in the end both solutions does not provide me a Docker image per release channel and are limited in their own way. It looks hacky to me. I bet there are better approaches or tools I could use. I don't mind switching to something else completely.
What would be the best approach to accomplish something like what CoreOS does?