Dockerizing a rails app part II
In this article I demonstrate how to set up a rails app to run capybara tests on a remote server using docker-compose. I also show you how to debug browser tests on your mac using its native vnc client.
In part we created a dockerfile for an existing rails app.
With that done we can now move on to getting our test suite running and passing.
Docker-compose
In order to run the test suite we need to bring up the full application stack. To orchestrate this we will use a python application called docker-compose.
Docker-compose commands require a configuration file for context, let's look at a working configuration file for our application's test environment.
# docker-compose.test.yml
version: "3.3"
services:
app:
build: .
image: seocahill/my-app:0.0.1
volumes:
- .:/app
environment:
- RAILS_ENV=test
- RUBYOPT="-W0"
depends_on:
- db
- selenium
db:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=cd_development
selenium:
image: selenium/standalone-firefox-debug
ports:
- '4444:4444'
- '5900:5900'
The app service
The build
command specifies that a fresh image should be built from the current directory's Dockerfile unless one exists in the cache, as specified in the image
command.
You can trigger a new build manually with:
docker-compose build .
Volumes are a method of sharing files and directories between the host (i.e you local dev environment) and a running container.
This syntax is: /host_dir:/container_dir
In this instance we are mounting the current directory (i.e. the application code) into the container running our application image at the same location that we used when we built the image initially:
# Dockerfile excerpt
# create and change into dir /app
WORKDIR /app
# copy host code in current host dir into current container dir
COPY . ./
As a consequence of this the application code on the host is used at runtime not the version we copied at buildtime. This allows us to avoid having to rebuild the image every time we change application code.
The environment
command accepts an array of environment variable key-values. Here I'm telling rails to use the test environment and not to log deprecation warnings to stdout when running the tests.
The depends_on
command tells the app service to hold off starting up until the selenium and db services are started.
Having the db
container started is not equivalent to having mysql started however! You can refer to this documentation for general tips on controlling service startup order.
The db service
The db service is derived from Mysql's official image.
The environment variables passed in for database name and root password are used to initialize the test database on startup
# database.yml
test:
adapter: mysql2
database: cd_test
encoding: utf8
host: db
password: root
username: root
The only thing out of the ordinary to note about the database configuration file is the host setting which is the same as the service name 'db'.
When binging up an application stack using docker-compose, docker automatically connects the running containers in the stack via a closed network and sets the hostnames of the containers within the network to the names of the services that spawned them.
This applies also when configuring sphinx access to mysql for indexing:
# thinking_sphinx.yml
sql_host: db
sql_port: 3306
The selenium service
In order to run integration tests rails needs access to a browser.
Selenium provides official images for the firefox and chrome browsers which you can find here.
Like most rails applications this one uses the capybara library to automate browser tests. In order to connect to a remotely running browser (i.e. in another docker host) we will need to tweak the capybara selenium driver configuration a little.
Configuring the driver
First you need to add the selenium-webdriver gem to your bundle if you haven't already.
Second add a custom driver configuration for the selenium service:
Capybara.register_driver "selenium_standalone_firefox".to_sym do |app|
Capybara::Selenium::Driver.new(
app, browser: :remote, url: "http://selenium:4444/wd/hub", desired_capabilities: :firefox
)
end
Capybara.javascript_driver = :selenium_standalone_firefox
Notice again that the service names double as hostnames within the application stack's docker network
The last step is to switch to this new driver when running browser based tests:
require 'socket'
Capybara.server_host = Socket.gethostname
Capybara.server_port = "3000"
Capybara.app_host = "http://#{Socket.gethostname}:3000"
Capybara.current_driver = :selenium_standalone_firefox
The server and app settings ensure capybara spawns the app's server on the same host and port as the urls it ultimately requests (random values are used otherwise).
I had to look up the app container's actual hostname as the "app" hostname wasn't resolving for some reason.
The last setting instructs Capybara to use the custom firefox driver defined earlier.
If you decide to use the remote driver for all Capybara tests (might be significantly slower) you should also turn off Capybara's bundled rack server which is booted by default:
Capybara.run_server = false
Running the test suite
To run the test suite execute:
docker-compose \
-f docker-compose-test.yml \
run --rm app \
ash ./script/run_tests.sh
The test script waits for the mysql test database to be initiated and then runs the test suite:
# script/run_tests.sh
#! /bin/ash
# wait for mysql to be available
printf '\nwaiting for mysql to boot\n';
TRIES=0
# repeatedly query mysql until the cd_test database has been initialized
until mysqlshow --host=db --user=root --password=root | grep cd_test || [ $TRIES -eq 10 ]; do
(( TRIES++ ))
printf '\nretrying in 2 seconds...\n';
sleep 2;
done
# Load schema and run tests or timeout after n unsuccessful healthcheck tries
if [ $TRIES -eq 15 ]; then
printf '\nno response from mysql'
else
printf '\ndb ready - loading in schema\n'
bin/rake db:schema:load
printf '\nrunning tests\n'
bin/rspec $@
fi
This test setup is ideal for CI as it starts from a completely clean slate - the only requirements to run the test suite are docker engine and docker-compose.
When running locally however you may want to speed things up a little.
One thing you can do is adding a named volume to persist the database and schema so don't have to recreate it on each run:
services:
db:
image: mysql:5.7
volumes:
- mysql:/var/lib/mysql
volumes:
:mysql
Debugging visually
The "debug" selenium docker image variants come bundled with a vnc server running on port 5900 to facilitate visual debugging.
The ports
command here forwards port 5900 on the selenium container to port 5900 on the host.
On mac we can setup the native vnc client to access the selenium images vnc server like so:
- With the selenium server running, open safari and enter the following url
vnc://localhost:5900
. - You will be prompted to authorized the vnc connection and then asked for a password, the default password is "secret".
Running a development stack
Most of the configuration and commands outlined above to get the test environment running applies directly to running the stack in development.
There are only two notable differences:
- Persisting data: You will probably want to persist database in a named volume as suggested earlier. You can read about how that's done in more detail here
- Running the stack: You will want to keep the stack up indefinitely using the
docker-compose up
command instead ofdocker-compose run
.
Wrap up and part III
In part III I'll look at configuring capistrano to work in a dockerized environment.