Say Goodbye to Docker Volumes 👋
Tags: docker, webdev, tutorial, devops
Published: 2023-11-27 14:40:05

Ever tried to use Docker volumes for hot-reloading in your web app? If you had the same horrible experience as me, you will enjoy the newest feature that Docker just released: **docker-compose watch**! Let me show you how to upgrade your existing project to have a wonderful Docker dev setup that your team will *actually* enjoy using 🤩

TL;DR: Check out this [docker-compose](https://github.com/Code42Cate/hackathon-starter/blob/main/docker-compose.yml) file and the [official documentation](https://docs.docker.com/compose/file-watch/)

Let's get started!


![spinning monke](https://media.giphy.com/media/e06Wc1bfzPQXnXyhLW/giphy.gif)

## Introduction 

Docker just released [Docker Compose Watch](https://docs.docker.com/compose/file-watch/) with [Docker Compose Version 2.22](https://docs.docker.com/compose/release-notes/#2220). With this new feature, you can use `docker-compose watch` instead of `docker-compose up` and automatically synchronize your local source code with the code in your Docker container without needing to use volumes!


Let us take a look at how this works in a real-word project by using a [project](https://github.com/Code42Cate/hackathon-starter) that I [previously wrote about](https://dev.to/code42cate/how-to-win-any-hackathon-3i99).

In this project, I have a monorepo with a frontend, backend, and some additional libraries for the UI and database.
```
├── apps
│   ├── api
│   └── web
└── packages
    ├── database
    ├── eslint-config-custom
    ├── tsconfig
    └── ui
```

Both apps (`api` and `web`) are already dockerized and the Dockerfiles are in the root of the project ([1](https://github.com/Code42Cate/hackathon-starter/blob/main/api.Dockerfile), [2](https://github.com/Code42Cate/hackathon-starter/blob/main/web.Dockerfile))


The `docker-compose.yml` file would look like this:
```yaml
services:
  web:
    build:
      dockerfile: web.Dockerfile
    ports:
      - "3000:3000"
    depends_on:
      - api
  api:
    build:
      dockerfile: api.Dockerfile
    ports:
      - "3001:3000"from within the Docker network
```

That's already pretty good, but as you already know it's a PITA to work with this during development. You will have to rebuild your Docker images whenever you change your code, even though your apps will probably support hot-reloading out of the box (or with something like [Nodemon](https://www.npmjs.com/package/nodemon) if not).

To improve this, Docker Compose Watch [introduces a new attribute](https://docs.docker.com/compose/file-watch/#configuration) called `watch`. The watch attribute contains a list of so-called **rules** that each contain a **path** that they are watching and an **action** that gets executed once a file in the path changes.

## Sync

If you would want to have a folder synchronized between your host and your container, you would add:
```yaml
services:
  web: # shortened for clarity
    build:
      dockerfile: web.Dockerfile
    develop:
      watch:
        - action: sync
          path: ./apps/web
          target: /app/apps/web
```

Whenever a file on your host in the path `./apps/web/` changes, it will get synchronized (copied) to your container to `/app/apps/web`. The additional app in the target path is required because this is our `WORKDIR` defined in the [Dockerfile](https://github.com/Code42Cate/hackathon-starter/blob/main/web.Dockerfile). This is the main thing you will probably use if you have hot-reloadable apps. 

## Rebuild

If you have apps that need to be compiled or dependencies that you need to re-install, there is also an action called **rebuild**. Instead of simply copying the files between the host and the container, it will rebuild and restart the container. This is super helpful for your npm dependencies! Let's add that:
```yaml
services:
  web: # shortened for clarity
    build:
      dockerfile: web.Dockerfile
    develop:
      watch:
        - action: sync
          path: ./apps/web
          target: /app/apps/web
        - action: rebuild
          path: ./package.json
          target: /app/package.json
```

Whenever our package.json changes we will now rebuild our entire Dockerfile to install the new dependencies.

## Sync+Restart

Besides just synchronizing and rebuilding there is also something in between called sync+restart. This action will first synchronize the directories and then immediately restart your container without rebuilding. Most frameworks usually have config files (such as `next.config.js`) that can't be hot-reloaded (just sync isn't enough) but also don't require a slow rebuild. 

This would change your compose-file to this:
```yaml
services:
  web: # shortened for clarity
    build:
      dockerfile: web.Dockerfile
    develop:
      watch:
        - action: sync
          path: ./apps/web
          target: /app/apps/web
        - action: rebuild
          path: ./package.json
          target: /app/package.json
        - action: sync+restart
          path: ./apps/web/next.config.js
          target: /app/apps/web/next.config.js
```

## Caveats

As always, there is no [free lunch](https://en.wikipedia.org/wiki/No_free_lunch_in_search_and_optimization) and a few caveats 😬

The biggest problem with the new `watch` attribute is that the paths are still very basic. The documentation states that Glob patterns are not supported yet which can result in a *huge* amount of rules if you want to be specific.

Here are some examples of what works and what does not:

✅ `apps/web`
This will match *all* the files in `./apps/web` (e.g. `./apps/web/README.md`, but also `./apps/web/src/index.tsx`)

❌ `build/**/!(*.spec|*.bundle|*.min).js`
Globs are sadly (not yet?) supported

❌ `~/Downloads`
All paths are relative to the project root!


## Next Steps

If you are still not happy with your Docker setup, there are still many ways to improve it! 

Collaboration is a big part of software development and [working in silos](https://www.personio.com/hr-lexicon/working-in-silos/) can be seriously damaging to your team. Slow Docker builds and complicated setups don't help! To counteract this and promote a culture of collaboration you can use Docker extensions such as [Livecycle](https://hub.docker.com/extensions/livecycle/docker-extension?utm_source=github&utm_medium=code42cate&utm_campaign=docker-compose-watch) to instantly share your local docker-compose apps with your teammates. Since you are already using Docker and docker-compose, all you need to do is install the [Docker Desktop Extension](https://hub.docker.com/extensions/livecycle/docker-extension?utm_source=github&utm_medium=code42cate&utm_campaign=hackathonstarter) and click on the share toggle. Your apps will then be tunneled to the internet and you can share your unique URL with your team to get feedback! I've written more about that in [this post](https://dev.to/code42cate/how-to-win-any-hackathon-3i99) if you want to check out more use cases of Livecycle :)

As always, make sure that your Dockerfile is following best practices, especially around multi-stage builds and caching. While this might make writing the initial Dockerfile harder, it will make your Docker apps a lot more pleasant to use during development.

Creating a basic `.dockerignore` file and separating dependency installation from code building goes a long way!


## Conclusion

As always, I hope you learned something new today! Let me know if you need any help setting up your Docker project, or if you have any other feedback

Cheers, Jonas :D