Docker compose watch recursively

Hi all,

I have been struggling to “watch” and “sync” when using docker compose up --watch recursively.
The watch does work when changing the files directly under ./app, like main.py and middleware.py
However, it doesn’t work when I change the files in subdirectories like ./app/routers

For the app, I am using FastAPI, a python framework.

Things I’ve tried

  1. remove the trailing slash
watch:
    - action: sync
          path: ./app
          target: /example/app
  1. add wildcard
watch:
    - action: sync
        path: ./app/*
        target: /example/app/*
  1. Add action for each directory
watch:
    - action: sync
        path: ./app/
        target: /example/app/

    - action: sync
        path: ./app/routers/
        target: /example/app/routers/

- action: sync
        path: ./app/services/
        target: /example/app/services/
  1. Add action for each file
watch:
    - action: sync
        path: ./app/
        target: /example/app/routers/router1.py

    - action: sync
        path: ./app/routers/
        target: /example/app/routers/router2.py
  1. Add “–reload-dir”, “/example/app” to the CMD in Dockerfile
  2. The move the CMD to docker compose file from Dockerfile
  3. Add WATCHFILES_FORCE_POLLING: true as an environment variable

None of these worked. Also, changing “sync” to “sync+restart” or “rebuild” didn’t work as well.
In the documentation, it says “Directories are watched recursively” so I thought this would work but I haven’t been successful.

Can you advise on what I am missing or doing anything wrong please?
Thank you for your help.

References I looked into

compose.yaml

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    container_name: api-example
    develop:
      watch:
        - action: sync
          path: ./app/
          target: /example/app/
    environment:
      WATCHFILES_FORCE_POLLING: true
    depends_on:
      - database

  database:
    image: postgres:16
    restart: always
    container_name: example-db
    environment:
      POSTGRES_DB: example
      POSTGRES_USER: example
      POSTGRES_PASSWORD: example
    volumes:
      - ./local_init.sql:/docker-entrypoint-initdb.d/init.sql

  flyway:
    image: flyway/flyway
    depends_on:
      - database
    container_name: flyway
    command: migrate
    environment:
      FLYWAY_URL: example
      FLYWAY_USER: example
      FLYWAY_PASSWORD: example
      FLYWAY_SCHEMAS: example
      FLYWAY_LOCATIONS: filesystem:/flyway/sql
    volumes:
      - ./app/database/migration:/flyway/sql

Dockerfile

FROM python:3.12-slim

WORKDIR /example
COPY ./requirements.txt ./requirements.txt

RUN pip install --no-cache-dir --upgrade -r ./requirements.txt

COPY ./app ./app
COPY ./logging_config.ini ./logging_config.ini

CMD [ "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"]

directory structure

.
├── app
│   ├── database
│   │   ├── database_connection_pool.py
│   │   ├── entities
│   │   │   ├── entity1.py
│   │   │   ├── entity2.py
│   │   │   ├── __init__.py
│   │   │   ├── entity3.py
│   │   │   └── entity4.py
│   │   ├── __init__.py
│   │   ├── migration
│   │   │   └── V1__migration1.sql
│   │   └── repositories
│   │       ├── __init__.py
│   │       └── repository.py
│   ├── dto
│   │   ├── dto1.py
│   │   ├── dto2.py
│   │   ├── __init__.py
│   │   ├── dto3.py
│   │   ├── dto4.py
│   │   ├── dto5.py
│   │   └── dto6.py
│   ├── __init__.py
│   ├── main.py
│   ├── middleware.py
│   ├── routers
│   │   ├── __init__.py
│   │   ├── router1.py
│   │   └── router2.py
│   ├── services
│       ├── service1.py
│       ├── service2.py
│       ├── __init__.py
│       ├── service3.py
│       ├── service4.py
│       └── service5.py
├── compose.yaml
├── Dockerfile
├── README.md
├── requirements.txt
├── init.sql
├── local_init.sql
├── logging_config.ini