On a project, the slowness of an operation is never a trivial detail: you pay for it on every iteration, several times a day, across the whole team. SQL migrations are a textbook example. As long as you have to rebuild a Docker image by hand and rerun it on every schema change, the smallest modification becomes a chore, and a chore you eventually start avoiding. Docker Compose’s Watch feature radically changes this dynamic. Here’s how it let us automate our SQL migrations and give the team back some time.
The context: a Node.js, .NET, and SQL Server combo
Our application stack is written mostly in Node.js and TypeScript. On the database side, however, we use SQL Server with the Microsoft SQL Project SDK, which lets you manage SQL Server projects from versioned .sql files. This SDK is powerful: it provides declarative schema management and a solid migration mechanism. But it comes with a strong constraint: it integrates primarily with the .NET ecosystem, whereas the rest of our code lives in the Node world.
To reconcile these two worlds without forcing every developer to install .NET and the SQL Server tooling locally, we wrapped everything in Docker. Concretely, two images:
- one image for the SQL Server database itself;
- one image to build and migrate the SQL project, bundling the .NET SDK and the required tools.
The benefit is obvious: anyone can run the database and apply the migrations without installing anything but Docker. The environment is reproducible and isolated.
The problem: a feedback loop that’s too long
This approach worked, but it carried a major flaw. On every change to a SQL file, the cycle was: manually rebuild the migration Docker image, then run it to propagate the change to the database. Nothing insurmountable in itself, but those are a few seconds, sometimes more, repeated dozens of times a day, on every schema tweak.
The result is insidious: friction discourages experimentation. You hesitate to split a migration into small steps, you batch changes together to amortize the rebuild cost, and the feedback loop that should be instantaneous becomes a bottleneck. The real technical question was this: how do you bridge SQL files that live on the host, on the developer’s machine, and the Docker services running isolated in their containers?
The solution: Docker Compose Watch
The answer arrived with Docker Compose version 2.22.0, which introduced the Watch feature. Its principle: watch files on the host side and automatically propagate their changes to the containers, according to a strategy you choose service by service. Three actions are available.
sync
The sync action syncs files between the host and the container without restarting the latter. It’s the ideal option for a web application that supports hot reload or live reload: the process inside the container detects the change and reloads on its own, with no need to restart the whole container.
rebuild
The rebuild action automatically rebuilds the image on every change, then restarts the container. You reserve it for cases where the image contents can’t simply be swapped in hot, for example when the change requires a recompilation, or when the application needs to be rebuilt to account for the modification. This is exactly the behavior our SQL migration image calls for.
sync+restart
The sync+restart action combines both logics: it syncs the files like sync, then restarts the container once the sync is done. It’s a perfect fit for configuration files, like an nginx.conf, which you just need to copy over and then reload via a process restart.
Setting up Watch on the migrations
The configuration is declared directly in the compose.yaml file, under a develop.watch key on the relevant service. For our migration image, we map the folder holding the SQL files to the rebuild action: as soon as a file changes on the host side, the image is rebuilt and the container restarted, which replays the migration against the database.
services:
sql-migrations:
build:
context: ./sql
depends_on:
- sqlserver
develop:
watch:
- path: ./sql
action: rebuild
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "Your_password123"
ports:
- "1433:1433"
Once the configuration is in place, all that’s left is to start Docker Compose in watch mode:
docker compose watch
From there, everything is automated. Every modification of a .sql file triggers the rebuild of the migration image and its execution, and the change is propagated straight to the database with no manual action. The feedback loop shrinks down to what it should always have been: you save the file, and the database is up to date.
The result: a more productive team
The most immediate effect is the disappearance of a repetitive manual step. But the real benefit lies elsewhere: by removing the friction, you change the team’s relationship to migrations. You iterate more willingly, you split schema changes more finely, you test more freely. An operation that had become a bottleneck goes back to being a formality.
It’s also a good reminder of a more general principle: automating what’s repetitive isn’t a tooling luxury, it’s a direct investment in the team’s velocity and comfort. Docker Compose Watch is just one tool among many, but it perfectly illustrates how a few well-placed lines of configuration can turn a daily chore into a non-event.
Conclusion
Docker Compose’s Watch feature, introduced in version 2.22.0, offers three sync strategies (sync, rebuild, and sync+restart) which you pick based on what the service actually does inside its container. For SQL migrations wrapped in an image, rebuild closes the loop between the host’s files and the database, and docker compose watch orchestrates the whole thing without intervention.
Beyond the SQL case, this is a reflex worth generalizing: every time a developer has to rebuild or restart something by hand after a file change, there’s probably a Watch action that can do it for them.