Local development workflow with Docker

Construction site
Figure 59 : Construction site

Objectives

At the end of this section, you will be able to:

  • Share code between container and host.

  • Use a simple local development workflow.

Local development in a container

We want to solve the following issues:

  • "Works on my machine"

  • "Not the same version"

  • "Missing dependency"

By using Docker containers, we will get a consistent development environment.

Working on the "namer" application

$ git clone https://github.com/jpetazzo/namer

Looking at the code

$ cd namer
$ ls -1
company_name_generator.rb
config.ru
docker-compose.yml
Dockerfile
Gemfile

Aha, a Gemfile! This is Ruby. Probably. We know this. Maybe?

Looking at the Dockerfile

FROM ruby

COPY . /src
WORKDIR /src
RUN bundler install

CMD ["rackup", "--host", "0.0.0.0"]
EXPOSE 9292
  • This application is using a base ruby image.
  • The code is copied in /src.
  • Dependencies are installed with bundler.
  • The application is started with rackup.
  • It is listening on port 9292.

Building and running the "namer" application

  • Let's build the application with the Dockerfile!
$ docker build -t namer .
  • Then run it. We need to expose its ports.
$ docker run -dP namer
  • Check on which port the container is listening.
$ docker ps -l

Connecting to our application

  • Point our browser to our Docker node, on the port allocated to the container.

  • Hit "reload" a few times.

  • This is an enterprise-class, carrier-grade, ISO-compliant company name generator!

    (With 50% more bullshit than the average competition!)

    (Wait, was that 50% more, or 50% less? Anyway!)

    web application 1
    Figure 60 : web application 1

Making changes to the code

Option 1:

  • Edit the code locally
  • Rebuild the image
  • Re-run the container

Option 2:

  • Enter the container (with docker exec)
  • Install an editor
  • Make changes from within the container

Option 3:

  • Use a volume to mount local files into the container
  • Make changes locally
  • Changes are reflected into the container

Our first volume

We will tell Docker to map the current directory to /src in the container.

$ docker run -d -v $(pwd):/src -P namer
  • -d: the container should run in detached mode (in the background).

  • -v: the following host directory should be mounted inside the container.

  • -P: publish all the ports exposed by this image.

  • namer is the name of the image we will run.

  • We don't specify a command to run because it is already set in the Dockerfile.

Note: on Windows, replace $(pwd) with %cd% (or ${pwd} if you use PowerShell).

Mounting volumes inside containers

The -v flag mounts a directory from your host into your Docker container.

The flag structure is:

[host-path]:[container-path]:[rw|ro]
  • If [host-path] or [container-path] doesn't exist it is created.

  • You can control the write status of the volume with the ro and rw options.

  • If you don't specify rw or ro, it will be rw by default.

There will be a full chapter about volumes!

Testing the development container

  • Check the port used by our new container.
$ docker ps -l
CONTAINER ID  IMAGE  COMMAND  CREATED        STATUS  PORTS                   NAMES
045885b68bc5  namer  rackup   3 seconds ago  Up ...  0.0.0.0:32770->9292/tcp ...
  • Open the application in your web browser.

Making a change to our application

Our customer really doesn't like the color of our text. Let's change it.

$ vi company_name_generator.rb

And change

color: royalblue;

To:

color: red;

Viewing our changes

  • Reload the application in our browser.

  • The color should have changed.

    web application 2
    Figure 61 : web application 2

Understanding volumes

  • Volumes are not copying or synchronizing files between the host and the container.

  • Volumes are bind mounts: a kernel mechanism associating a path to another.

  • Bind mounts are kind of similar to symbolic links, but at a very different level.

  • Changes made on the host or on the container will be visible on the other side.

    (Since under the hood, it's the same file on both anyway.)

Trash your servers and burn your code

(This is the title of a 2013 blog post by Chad Fowler, where he explains the concept of immutable infrastructure.)

  • Let's mess up majorly with our container.

    (Remove files or whatever.)

  • Now, how can we fix this?

  • Our old container (with the blue version of the code) is still running.

  • See on which port it is exposed:

    docker ps
    
  • Point our browser to it to confirm that it still works fine.

Immutable infrastructure in a nutshell

  • Instead of updating a server, we deploy a new one.

  • This might be challenging with classical servers, but it's trivial with containers.

  • In fact, with Docker, the most logical workflow is to build a new image and run it.

  • If something goes wrong with the new image, we can always restart the old one.

  • We can even keep both versions running side by side.

If this pattern sounds interesting, you might want to read about blue/green deployment and canary deployments.

Recap of the development workflow

  1. Write a Dockerfile to build an image containing our development environment.
    (Rails, Django, ... and all the dependencies for our app)

  2. Start a container from that image.
    Use the -v flag to mount our source code inside the container.

  3. Edit the source code outside the containers, using regular tools.
    (vim, emacs, textmate...)

  4. Test the application.
    (Some frameworks pick up changes automatically.
    Others require you to Ctrl-C + restart after each modification.)

  5. Iterate and repeat steps 3 and 4 until satisfied.

  6. When done, commit+push source code changes.

Debugging inside the container

Docker has a command called docker exec.

It allows users to run a new process in a container which is already running.

If sometimes you find yourself wishing you could SSH into a container: you can use docker exec instead.

You can get a shell prompt inside an existing container this way, or run an arbitrary process for automation.

docker exec example

$ # You can run ruby commands in the area the app is running and more!
$ docker exec -it <yourContainerId> bash
root@5ca27cf74c2e:/opt/namer# irb
irb(main):001:0> [0, 1, 2, 3, 4].map {|x| x ** 2}.compact
=> [0, 1, 4, 9, 16]
irb(main):002:0> exit

Stopping the container

Now that we're done let's stop our container.

$ docker stop <yourContainerID>

And remove it.

$ docker rm <yourContainerID>

Section summary

We've learned how to:

  • Share code between container and host.

  • Set our working directory.

  • Use a simple local development workflow.