Software localization
Unifying Ruby on Rails Environments with Docker Compose
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.
Last updated on October 27, 2022.