In Progress
Unit 1, Lesson 1
In Progress

A Quick Dev Container

Using containers for development doesn’t have to involve writing a bunch of configuration. In today’s episode we’ll go from zero to running Rails tests inside a deccontainer, with nary a line of YAML!

Video transcript & code

As I talked about in another video, I'm kicking off a new series of videos about container-based development. I want to show you how to build project-specific developer infrastructure as code, in the form of a set of versioned docker configuration files.

But before we get started building configuration files, I thought we'd start off a little more basic. Today, we're only going to start containers from the command-line.

Here we have a Debian Linux machine that's pretty much pristine.

We don't have a compiler.

avdi@viv:~$ gcc
bash: gcc: command not found

We don't have any dynamic language runtimes.

avdi@viv:~$ python --help
bash: python: command not found
avdi@viv:~$ node --help
bash: node: command not found
avdi@viv:~$ ruby --version
bash: ruby: command not found

What we do have is Git.

avdi@viv:~/rake$ git --version
git version 2.20.1

And Docker.

avdi@viv:~/rake$ docker --version
Docker version 20.10.0, build 7287ab3

We want to clone a Rails project and make some changes to it. But we don't want to spend a lot of time getting set up.

git clone https://github.com/gracefuldev/sixmilebridge.git

We know that to make this work, at the very least we're going to need Ruby.

cd sixmilebridge/

And we know from past experience that whatever the system-packaged version of Ruby is on this machine, it's probably not going to be the version the project expects.

avdi@viv:~/sixmilebridge$ head Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '~> 2.7.2'

So we know that at the very least, we're going to need to install some software to build ruby, then we'll need to build and install the right version of Ruby. Which probably means setting up a Ruby version manager like RVM or rbenv. Ugh, so many prerequisite steps!

But there's another way. There's a way to get started hacking on this project immediately, inside a computer that is already configured for Ruby development with the version of Ruby that we need.

We'll do our development inside a Docker container. We go to the Docker Hub website and find that there's a preexisting image for running Ruby programs.

And there's a tagged version of it that corresponds to the version this project needs.

So in our Terminal, we tell Docker we want to run a new container.

That it should run the command we give it interactively.

That it should provide us with a virtual terminal, not just basic input/output.

Then we tell it that we want bind a volume, which in Docker-speak means we want to share a directory from the host machine---the one we're in now---with the container.

The directory we want to share is the current one. Docker insists on fully-qualified paths, so we give it the output of the pwd command rather than just a dot.

We tell it that this directory should appear at the path /workspace inside the container.

We also specify that we would like our working directory inside the container to be this mapped-in /workspace directory.

Now we specify the image we want this container to be based on. We choose ruby:2.7.2, the base image we discovered on Docker Hub.

Finally, we tell Docker the initial command we want to run inside this container. We want a Bash shell. We specify -l to tell it to be a login shell.

docker run -i -t --volume `pwd`:/workspace -w /workspace ruby:2.7.2 /bin/bash -l

Docker downloads what it needs to build a new container based on the ruby:2.7.2 image. In this case that's already cached because I've run this example before. So it immediately starts a container, and executes our command. Now we're in a new shell, inside the container!

In here, we have the needed version of Ruby, as well as other essential Ruby development tools like rake and bundler.

root@9cf7315d248e:/workspace# ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
root@9cf7315d248e:/workspace# rake --version
rake, version 13.0.1
root@9cf7315d248e:/workspace# bundle --version
Bundler version 2.1.4

When we run ls, we can see that we're still inside our Rails project root, even though we're working inside the container now. That's thanks to the volume mapping we specified in the docker command line!

Our goal right now is just to get to the point where we can run the project's tests successfully. Let's start installing Ruby package dependencies with a bundle install

root@f5d095b5cccc:/workspace# bundle install

Once that's done, we'll set up the project's database. Fortunately this project defaults to SQLite for development and test, so we don't need to worry about getting a database server up and running.

root@f5d095b5cccc:/workspace# bundle exec rails db:setup
Libhoney::Client: no writekey configured, disabling sending events
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'

We do, however, need to get the JavaScript side of the project set up.

Rails uses the yarn package manager by default, so we install the Debian package for Yarn.

root@f5d095b5cccc:/workspace# apt install yarnpkg

The Debian packaging disambiguates the yarn command name from... whatever other historical package has a command named yarn. It does this by renaming it yarnpkg.

root@f5d095b5cccc:/workspace# yarnpkg --version
1.13.0

We just want to type yarn though, and some Rails commands expect it to be called yarn.

Fortunately, this is a pretend computer that exists only for this one project! So we don't have any hesitation adding a new global symlink into /usr/local/bin.

root@f5d095b5cccc:/workspace# ln -s /usr/bin/yarnpkg /usr/local/bin/yarn

now we can fetch javascript dependencies by invoking yarn install.

root@f5d095b5cccc:/workspace# yarn install
yarn install v1.13.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.3: The platform "linux" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "linux" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > webpack-dev-server@3.11.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
[4/4] Building fresh packages...
warning Your current version of Yarn is out of date. The latest version is "1.22.5", while you're on "1.13.0".
Done in 40.87s.

And we can tell Rails to precompile JavaScript assets.

root@f5d095b5cccc:/workspace# bundle exec rails webpacker:compile
Libhoney::Client: no writekey configured, disabling sending events
Compiling...
Compiled all packs in /workspace/public/packs
Hash: 666d45eeef50ba415ec8
Version: webpack 4.44.2
Time: 5918ms
Built at: 12/11/2020 6:09:02 PM
                                        Asset       Size  Chunks                         Chunk Names
       js/application-dd6e88065a32b23f8e21.js   69.3 KiB       0  [emitted] [immutable]  application
    js/application-dd6e88065a32b23f8e21.js.br   15.3 KiB          [emitted]
    js/application-dd6e88065a32b23f8e21.js.gz   17.7 KiB          [emitted]
   js/application-dd6e88065a32b23f8e21.js.map    205 KiB       0  [emitted] [dev]        application
js/application-dd6e88065a32b23f8e21.js.map.br     44 KiB          [emitted]
js/application-dd6e88065a32b23f8e21.js.map.gz     51 KiB          [emitted]
                                manifest.json  364 bytes          [emitted]
                             manifest.json.br  129 bytes          [emitted]
                             manifest.json.gz  142 bytes          [emitted]
Entrypoint application = js/application-dd6e88065a32b23f8e21.js js/application-dd6e88065a32b23f8e21.js.map
[0] (webpack)/buildin/module.js 552 bytes {0} [built]
[1] ./app/javascript/packs/application.js 742 bytes {0} [built]
[5] ./app/javascript/channels/index.js 205 bytes {0} [built]
[6] ./app/javascript/channels sync _channel\.js$ 160 bytes {0} [built]
    + 3 hidden modules

With these preliminaries out of the way, we can kick off the project's tests...

root@f5d095b5cccc:/workspace# bundle exec rails test
Running via Spring preloader in process 13494
Run options: --seed 57461

# Running:

..........

Finished in 4.290701s, 2.3306 runs/s, 2.7967 assertions/s.
10 runs, 12 assertions, 0 failures, 0 errors, 0 skips

All green!

So, this wasn't a completely painless setup, but by using a docker container we were able to skip a fair amount of package installation and Ruby compilation.

OK, but what about developing inside this container? Does that mean we can only use text editors that the container has installed? Does this container even have an editor??

Not to worry!

Let's exit the container, and then let's fire up a text editor.

$ exit
avdi@viv:~/sixmilebridge$ code .

And let's add a failing test.

require 'test_helper'

class IslanderTest < ActiveSupport::TestCase
  test "black is white" do
    assert_equal true, false
  end
end

Now let's get back inside the container...

avdi@viv:~/sixmilebridge$ docker run -i -t --volume `pwd`:/workspace -w /workspace ruby:2.7.2 /bin/bash -l

And let's run the tests again...

root@05c941853294:/workspace# bundle exec rails test
bundler: command not found: rails
Install missing gem executables with `bundle install`

Wait, what? What happened to Rails???

Here's the thing: docker run creates a new container and runs a command inside it. So all those setup steps we did were on a different container. In effect, they were on a different machine!

On the one hand, this is great! It makes it really easy to try out new system configurations without fear of messing up our machine. But it also makes it really easy to lose our work setting up a system just right.

So do we have to do all that work over again? Fortunately not.

If we run docker ps -a we can see a list of containers, sorted by recency.

The one before last is the one we configured for this project.

avdi@viv:~/sixmilebridge$ docker ps -a
CONTAINER ID   IMAGE                                                              COMMAND                  CREATED          STATUS                         PORTS     NAMES
05c941853294   ruby:2.7.2                                                         "/bin/bash -l"           5 minutes ago    Exited (127) 5 seconds ago               great_kapitsa
9cf7315d248e   ruby:2.7.2                                                         "/bin/bash -l"           48 minutes ago   Exited (1) 38 minutes ago                epic_khayyam

Let's copy its automatically-assigned codename.

Then let's start that container back up with docker start.

avdi@viv:~/sixmilebridge$ docker start upbeat_pascal
upbeat_pascal

And then instead of a docker run command...

We'll use docker exec to run a command inside an existing container.

We remove the --volume mapping, which only needs to be run on container initialization.

And in place of the base image, we'll paste that container code name.

avdi@viv:~/sixmilebridge$ docker exec -i -t -w /workspace upbeat_pascal /bin/bash -l

And now we're back inside our original container!

What were we gonna do again?

Oh yeah, run the tests!

root@f5d095b5cccc:/workspace# bundle exec rails test
Libhoney::Client: no writekey configured, disabling sending events
Running via Spring preloader in process 27
Run options: --seed 14392

# Running:

...F

Failure:
IslanderTest#test_black_is_white [/workspace/test/models/islander_test.rb:9]:
Expected: true
  Actual: false


rails test test/models/islander_test.rb:8

.......

Finished in 0.414483s, 26.5391 runs/s, 31.3643 assertions/s.
11 runs, 13 assertions, 1 failures, 0 errors, 0 skips

And there we go, there's our one failing test that we added

What this shows is that we were able to edit the project outside our container, but run the code inside, where we have a Ruby development environment set up!

So today we've seen some basics of using a container for development without touching any configuration files. We saw how it let us skip some, but not all, of the environment setup for a Rails project.

In upcoming videos, we'll start using docker and docker-compose configurations inside the projects to automate even more of the development environment setup. Happy hacking!

Responses