Rebuilding the image every time we make changes is tedious and slows us down in our development workflow. But Docker wouldn't be Docker if we couldn't work around this problem. The solution here are so called bind mounts. A bind mount allows us to mount a local file or directory into the containers file system. For the Docker CLI, we can specify the bind mount using the -v
flag. With Docker Compose, we simply add the definition the docker-compose.yml
via the volumes
directive. Here is an example:
app:
image: your_docker_id/rails_app:v1
build:
context: .
environment:
- POSTGRES_HOST=pg
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=secret
- RAILS_ENV
volumes:
- ./:/usr/src/app:cached
ports:
- 127.0.0.1:3000:3000
The key part here is the volumes
definition:
volumes:
- ./:/usr/src/app:cached
We instruct Docker Compose to mount the current local working directory (./
) to the /usr/src/app
directory in the container. The /usr/src/app
directory in the image contains a copy of our source code. We essentially just "replace" the content with what is currently on our local file system.
Note: The
cached
options will increase the performance of the bind mount on MacOS. Unlike on Linux, there is some overhead when using bind mounts on MacOS. Remember, Linux containers need to run on Linux and Docker will setup a virtual machine running Linux on your Mac. Getting the data from your Mac into the virtual machine requires a shared file system calledosxfs
. There are significant overheads to guaranteeing perfect consistency and thecached
options looses up the those guarantees. We don't require perfect consistency for our use case: Mounting our source code into the container.
In addition to the bind mount we will also add a volume for our tmp/
directory. This is not strictly required but recommended for the following reasons:
- On MacOS a volume will be a lot faster than a bind mount. Since we Rails will read and write temporary and cache data to
tmp/
we want to give ourselves maximum speed. - We don't usually access anything in the
tmp/
directory locally, so there is no reason to write the data back to our Docker Host. - Bootsnap doesn't seem to play nice with the
cached
option and using a dedicated volume solves this problem for us.
In our app
service definition we will mount a volume to /usr/src/app/tmp
:
volumes:
- ./:/usr/src/app:cached
- tmp:/usr/src/app/tmp
Since we are using a named volume we also have to add it to the volumes
section at the bottom of our docker-compose.yml
:
volumes:
pg-data:
tmp:
There is one more change we have to make to our Dockerfile
. Since every container has its own process tree by default, the Rails server will always be PID 1. Puma, our Web Server, stores a PID-file in tmp/
. This leads to issues when we restart our container: Puma will see the PID file and because there is already a process with the PID 1 (Puma itself), Puma will refuse to start and exit. We can work around this issue by appending the --pid=/tmp/server.pid
flag to rails server
. In order to do that we will change the CMD
instruction in the Dockerfile
like so:
CMD ["rails", "server", "-b", "0.0.0.0", "--pid=/tmp/server.pid"]
Check out _examples/docker-compose.yml.with_bind_mount
and _examples/Dockerfile.with_server_pid
for complete examples.
We can now restart our containers and rebuild the image in one go with:
docker-compose up -d --build
Docker Compose will pick up our changes and re-create the container for our app
service and create the new tmp
volume. Make sure that everything is running with docker-compose ps
and use docker-compose logs
to troubleshoot any issues.
With the bind mount in place, we can start iterating on our application. Here are a few things you can try:
- Make some changes to
app/views/todos/index.html.erb
orapp/views/todos/_form.html.erb
. You can for example change a copy or the class of a submit button (trybtn-secondary
instead ofbtn-primary
) - Create a new model using the Rails generators
- Add a
presence
validation toapp/models/activity.rb
for thedata
field.
You should see the changes being reflected in the already running containers without the need to rebuild or restart anything.
You can find our changes in the iterating
branch. Compare it to the previous branch to see what changed.