Dockerfile
FROM ruby:2.4.1
# add nodejs and yarn dependencies for the frontend
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
# install bundler in specific version
RUN gem install bundler --version "1.15.3"
# install required system packages for ruby, rubygems and webpack
RUN apt-get update && apt-get upgrade -y && \
apt-get install --no-install-recommends -y ca-certificates nodejs yarn \
libicu-dev imagemagick unzip qt5-default libqt5webkit5-dev \
gstreamer1.0-plugins-base gstreamer1.0-tools gstreamer1.0-x \
xvfb xauth openjdk-7-jre --fix-missing
RUN mkdir -p /app
WORKDIR /app
# install node dependencies (e.g. webpack)
# next two steps will be cached unless either package.json or
# yarn.lock changes
COPY package.json yarn.lock /app/
RUN yarn install
# bundle gem dependencies
# next two steps will be cached unless Gemfile or Gemfile.lock changes.
# -j $(nproc) runs bundler in parallel with the amount of CPUs processes
COPY Gemfile Gemfile.lock /app/
RUN bundle install -j $(nproc)
# docker-compose.yml
version: '2.1'
services:
app: &app
build:
context: .
dockerfile: Dockerfile.test
volumes: [".:/app"] # mount current directory into the image
# use tmpfs for tmp and log for performance and to allow
# multiple builds in parallel. Both directories are mounted
# into the image AFTER the working directory is mounted.
tmpfs: ["/app/tmp", "/app/log"]
dev: &dev
<<: *app
environment:
RAILS_ENV: "development"
DATABASE_URL: "mysql2://mysql/phraseapp?local_infile=true"
ELASTIC_SEARCH_URL: http://elasticsearch:9200
REDIS_URL: "redis://redis"
depends_on:
mysql: {"condition":"service_healthy"}
redis: {"condition":"service_healthy"}
elasticsearch: {"condition":"service_healthy"}
server:
<<: *dev
command: ["bundle", "exec", "./build/validate-migrated.sh && rails server -b 0.0.0.0"]
ports: ["3000:3000"]
test: &test
<<: *app
environment:
RAILS_ENV: "test"
DATABASE_URL: "mysql2://mysql-test/phraseapp?local_infile=true"
ELASTIC_SEARCH_URL: http://elasticsearch-test:9200
REDIS_URL: "redis://redis-test"
SPRING_TMP_PATH: "/app/tmp"
# wait for all dependent services to be healthy
depends_on:
mysql-test: {"condition":"service_healthy"}
redis-test: {"condition":"service_healthy"}
elasticsearch-test: {"condition":"service_healthy"}
# allow executing of single tests against a running spring server
spring:
<<: *test
command: ["bundle", "exec", "./build/validate-migrated.sh && spring server"]
elasticsearch: &elasticsearch
image: elasticsearch:1.7.6
ports: ["9200"]
healthcheck:
test: ["CMD", "curl", "-SsfL", "127.0.0.1:9200/_status"]
interval: 1s
timeout: 1s
retries: 300
elasticsearch-test:
<<: *elasticsearch
# place elasticsearch data on tmpfs for performance
tmpfs: /usr/share/elasticsearch/data
redis: &redis
image: redis:2.8.23
ports: ["6379"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
timeout: 1s
retries: 300
redis-test:
<<: *redis
mysql: &mysql
image: mysql:5.6.35
ports: ["3306"]
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
MYSQL_DATABASE: "phraseapp"
healthcheck:
test: ["CMD", "mysql", "-u", "root", "-e", "select 1"]
interval: 1s
timeout: 1s
retries: 300
mysql-test:
<<: *mysql
tmpfs: /var/lib/mysql # place mysql on tmpfs for performance
All infrastructure services for our development environment and our integration tests (MySQL, ElasticSearch and Redis) have health checks configured and so the spring, test, dev and server services are only started when all of them are healthy.
As all services are placed in a dedicated docker network they can be accessed just by their name (e.g. DATABASE_URL=”mysql2://mysql/phraseapp_test?local_infile=true”).
There are dedicated services for the infrastructure components for the development and test environments. The spring and server services use a little script (validate-migrated.sh) to determine if migrations were already executed. If yes, we run possibly pending migrations rails db:migrate, otherwise we execute the more efficient rails db:setup.
# build/validate-migrated.sh if rails db:migrate:status &> /dev/null; then rails db:migrate else rails db:setup fi
Running tests
We can then trigger a full test run like this:
docker-compose build test docker-compose run test bundle exec "rails db:setup && xvfb-run rails spec" docker-compose down
#!/bin/bash
set -e
# random name with timestamp as prefix to give a cleanup script more information
PROJECT_NAME=$(TZ=UTC date +"%Y%m%d%H%M%S")$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8| head -n 1)
function cleanup {
# capture the original exit code
code=$?
echo "cleaning up"
# ignore errors on docker-compose down
set +e
docker-compose --project-name ${PROJECT_NAME} down
# exit with original exit code
exit $code
}
# run cleanup when the script exits
trap cleanup EXIT
docker-compose --project-name ${PROJECT_NAME} build test
docker-compose --project-name ${PROJECT_NAME} run test "$@"
docker-compose up --build spring
docker-compose exec spring xvfb-run spring rspec spec/models/user_spec.rb
alias dc-spec='docker-compose exec spring xvfb-run spring rspec' dp-spec spec/models/user_spec.rb
Running the development server is as simple as executing
docker-compose up server
By exposing port 3000 of the container we can access the server running inside docker at http://localhost:3000.





