pythondockerdocker-composehypercorn

Hypercorn with "--reload" and Docker volumes


I am running Hypercorn with --reload inside a Docker container. The file I am running is kept in a volume managed by Docker Compose.

When I change the file on my system, I can see that the change is reflected in the volume, e.g. with docker compose exec myapp /bin/cat /app/runtime/service.py.

However, when I change a file in this way, Hypercorn does not restart as I would have expected. Is there some adverse interaction between Hypercorn and the Docker volume? Or am I expecting something from the --reload option that I should not expect?

Example files are below. My expectation was that modifying runtime/service.py from outside the container would trigger Hypercorn to restart the server with the modified version of the file. But this does not occur.

Edit: I should add that I am using Docker 20.10.5 via Docker Desktop for Mac, on MacOS 10.14.6.

Edit 2: This might be a Hypercorn bug. If I add uvicorn[standard] in requirements.txt and run python -m uvicorn --reload --host 0.0.0.0 --port 8001 service:app, the reloading works fine. Possibly related: https://gitlab.com/pgjones/hypercorn/-/issues/185

entrypoint.sh:

#!/bin/sh
cd /app/runtime
/opt/venv/bin/python -m hypercorn --reload --bind 0.0.0.0:8001 service:app

Dockerfile:

FROM $REDACTED

RUN /opt/venv/bin/python -m pip install -U pip
RUN /opt/venv/bin/pip install -U setuptools wheel

COPY requirements.txt /app/requirements.txt
RUN /opt/venv/bin/pip install -r /app/requirements.txt

COPY requirements-dev.txt /app/requirements-dev.txt
RUN /opt/venv/bin/pip install -r /app/requirements-dev.txt

COPY entrypoint.sh /app/entrypoint.sh

EXPOSE 8001/tcp

CMD ["/app/entrypoint.sh"]

docker-compose.yml:

version: "3.8"
services:
  api:
    container_name: api
    hostname: myapp
    build:
      context: .
    ports:
      - 8001:8001
    volumes:
      - ./runtime:/app/runtime

runtime/service.py:

import logging
import quart

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

app = quart.Quart(__name__)

@app.route('/')
async def handle_hello():
    logger.info('Handling request.')
    return 'Hello, world!\n'

@app.route('/bad')
async def handle_bad():
    logger.critical('Bad request.........')
    raise RuntimeError('Oh no!!!')

Solution

  • Here is a minimal, fully working example which does auto-reload using hypercorn:

    docker-compose.yaml

    services:
      app:
        build: .
        # Here --reload is used which works as intended!
        command: hypercorn --bind 0.0.0.0:8080 --reload src:app
        ports:
          - 8080:8080
        volumes:
          - ./src:/app/src
    

    Dockerfile

    FROM python:3.10-slim-bullseye
    WORKDIR /app                                                          
    RUN pip install hypercorn==0.14.3 quart==0.18.0
    COPY src ./src
    EXPOSE 8080
    ENV QUART_APP=src:app
    # This is the production command; docker-compose.yaml overwrites it for local development
    CMD hypercorn --bind 0.0.0.0:8080 src:app
    

    src/__init__.py

    from quart import Quart
    
    app=Quart(__name__)
    
    @app.route('/', methods=['GET'])
    def get_root():
        return "Hello world!"
    

    Run via docker-compose up and notice how hypercorn reloads once __init__.py got modified.