Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto reload for development #6

Merged
merged 5 commits into from
Mar 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ Then you can build your image from the directory that has your `Dockerfile`, e.g
docker build -t myimage ./
```

* Run a container based on your image:

```bash
docker run -d --name mycontainer -p 80:80 myimage
```

You should be able to check it in your Docker container's URL, for example: http://192.168.99.100/ or http://127.0.0.1/ (or equivalent, using your Docker host).

## Advanced usage

### Environment variables
Expand Down Expand Up @@ -333,6 +341,57 @@ If you need to run a Python script before starting the app, you could make the `
python /app/my_custom_prestart_script.py
```

### Development live reload

The default program that is run is at `/start.sh`. It does everything described above.

There's also a version for development with live auto-reload at:

```bash
/start-reload.sh
```

#### Details

For development, it's useful to be able to mount the contents of the application code inside of the container as a Docker "host volume", to be able to change the code and test it live, without having to build the image every time.

In that case, it's also useful to run the server with live auto-reload, so that it re-starts automatically at every code change.

The additional script `/start-reload.sh` runs Uvicorn alone (without Gunicorn) and in a single process.

It is ideal for development.

#### Usage

For example, instead of running:

```bash
docker run -d -p 80:80 myimage
```

You could run:

```bash
docker run -d -p 80:80 -v $(pwd):/app myimage /start-reload.sh
```

* `-v $(pwd):/app`: means that the directory `$(pwd)` should be mounted as a volume inside of the container at `/app`.
* `$(pwd)`: runs `pwd` ("print working directory") and puts it as part of the string.
* `/start-reload.sh`: adding something (like `/start-reload.sh`) at the end of the command, replaces the default "command" with this one. In this case, it replaces the default (`/start.sh`) with the development alternative `/start-reload.sh`.

#### Technical Details

As `/start-reload.sh` doesn't run with Gunicorn, any of the configurations you put in a `gunicorn_conf.py` file won't apply.

But these environment variables will work the same as described above:

* `MODULE_NAME`
* `VARIABLE_NAME`
* `APP_MODULE`
* `HOST`
* `PORT`
* `LOG_LEVEL`


## Tests

Expand All @@ -341,6 +400,10 @@ All the image tags, configurations, environment variables and application option

## Release Notes

### 0.4.0

* Add support for live auto-reload with an additional custom script `/start-reload.sh`. PR <a href="https://github.com/tiangolo/uvicorn-gunicorn-docker/pull/6" target="_blank">#6</a>.

### 0.3.0

* Set `WORKERS_PER_CORE` by default to `1`, as it shows to have the best performance on benchmarks.
Expand Down
3 changes: 3 additions & 0 deletions python3.6-alpine3.8/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ RUN chmod +x /start.sh

COPY ./gunicorn_conf.py /gunicorn_conf.py

COPY ./start-reload.sh /start-reload.sh
RUN chmod +x /start-reload.sh

COPY ./app /app
WORKDIR /app/

Expand Down
28 changes: 28 additions & 0 deletions python3.6-alpine3.8/start-reload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#! /usr/bin/env sh
set -e

if [ -f /app/app/main.py ]; then
DEFAULT_MODULE_NAME=app.main
elif [ -f /app/main.py ]; then
DEFAULT_MODULE_NAME=main
fi
MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME}
VARIABLE_NAME=${VARIABLE_NAME:-app}
export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}

HOST=${HOST:-0.0.0.0}
PORT=${PORT:-80}
LOG_LEVEL=${LOG_LEVEL:-info}

# If there's a prestart.sh script in the /app directory, run it before starting
PRE_START_PATH=/app/prestart.sh
echo "Checking for script in $PRE_START_PATH"
if [ -f $PRE_START_PATH ] ; then
echo "Running script $PRE_START_PATH"
. "$PRE_START_PATH"
else
echo "There is no script $PRE_START_PATH"
fi

# Start Uvicorn with live reload
exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE"
3 changes: 3 additions & 0 deletions python3.6/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ RUN chmod +x /start.sh

COPY ./gunicorn_conf.py /gunicorn_conf.py

COPY ./start-reload.sh /start-reload.sh
RUN chmod +x /start-reload.sh

COPY ./app /app
WORKDIR /app/

Expand Down
28 changes: 28 additions & 0 deletions python3.6/start-reload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#! /usr/bin/env sh
set -e

if [ -f /app/app/main.py ]; then
DEFAULT_MODULE_NAME=app.main
elif [ -f /app/main.py ]; then
DEFAULT_MODULE_NAME=main
fi
MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME}
VARIABLE_NAME=${VARIABLE_NAME:-app}
export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}

HOST=${HOST:-0.0.0.0}
PORT=${PORT:-80}
LOG_LEVEL=${LOG_LEVEL:-info}

# If there's a prestart.sh script in the /app directory, run it before starting
PRE_START_PATH=/app/prestart.sh
echo "Checking for script in $PRE_START_PATH"
if [ -f $PRE_START_PATH ] ; then
echo "Running script $PRE_START_PATH"
. "$PRE_START_PATH"
else
echo "There is no script $PRE_START_PATH"
fi

# Start Uvicorn with live reload
exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE"
3 changes: 3 additions & 0 deletions python3.7-alpine3.8/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ RUN chmod +x /start.sh

COPY ./gunicorn_conf.py /gunicorn_conf.py

COPY ./start-reload.sh /start-reload.sh
RUN chmod +x /start-reload.sh

COPY ./app /app
WORKDIR /app/

Expand Down
28 changes: 28 additions & 0 deletions python3.7-alpine3.8/start-reload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#! /usr/bin/env sh
set -e

if [ -f /app/app/main.py ]; then
DEFAULT_MODULE_NAME=app.main
elif [ -f /app/main.py ]; then
DEFAULT_MODULE_NAME=main
fi
MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME}
VARIABLE_NAME=${VARIABLE_NAME:-app}
export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}

HOST=${HOST:-0.0.0.0}
PORT=${PORT:-80}
LOG_LEVEL=${LOG_LEVEL:-info}

# If there's a prestart.sh script in the /app directory, run it before starting
PRE_START_PATH=/app/prestart.sh
echo "Checking for script in $PRE_START_PATH"
if [ -f $PRE_START_PATH ] ; then
echo "Running script $PRE_START_PATH"
. "$PRE_START_PATH"
else
echo "There is no script $PRE_START_PATH"
fi

# Start Uvicorn with live reload
exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE"
3 changes: 3 additions & 0 deletions python3.7/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ RUN chmod +x /start.sh

COPY ./gunicorn_conf.py /gunicorn_conf.py

COPY ./start-reload.sh /start-reload.sh
RUN chmod +x /start-reload.sh

COPY ./app /app
WORKDIR /app/

Expand Down
28 changes: 28 additions & 0 deletions python3.7/start-reload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#! /usr/bin/env sh
set -e

if [ -f /app/app/main.py ]; then
DEFAULT_MODULE_NAME=app.main
elif [ -f /app/main.py ]; then
DEFAULT_MODULE_NAME=main
fi
MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME}
VARIABLE_NAME=${VARIABLE_NAME:-app}
export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}

HOST=${HOST:-0.0.0.0}
PORT=${PORT:-80}
LOG_LEVEL=${LOG_LEVEL:-info}

# If there's a prestart.sh script in the /app directory, run it before starting
PRE_START_PATH=/app/prestart.sh
echo "Checking for script in $PRE_START_PATH"
if [ -f $PRE_START_PATH ] ; then
echo "Running script $PRE_START_PATH"
. "$PRE_START_PATH"
else
echo "There is no script $PRE_START_PATH"
fi

# Start Uvicorn with live reload
exec uvicorn --reload --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE"
Empty file.
70 changes: 70 additions & 0 deletions tests/test_03_reload/test_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import time

import docker
import pytest
import requests
from docker.models.containers import Container

from ..utils import CONTAINER_NAME, get_logs, remove_previous_container

client = docker.from_env()


def verify_container(container, response_text):
logs = get_logs(container)
assert "Checking for script in /app/prestart.sh" in logs
assert "Running script /app/prestart.sh" in logs
assert (
"Running inside /app/prestart.sh, you could add migrations to this file" in logs
)
assert "Uvicorn running on http://0.0.0.0:80" in logs
response = requests.get("http://127.0.0.1:8000")
assert response.text == response_text


@pytest.mark.parametrize(
"image,response_text",
[
(
"tiangolo/uvicorn-gunicorn:python3.6",
"Hello world! From Uvicorn with Gunicorn. Using Python 3.6",
),
(
"tiangolo/uvicorn-gunicorn:python3.7",
"Hello world! From Uvicorn with Gunicorn. Using Python 3.7",
),
(
"tiangolo/uvicorn-gunicorn:latest",
"Hello world! From Uvicorn with Gunicorn. Using Python 3.7",
),
(
"tiangolo/uvicorn-gunicorn:python3.6-alpine3.8",
"Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.6",
),
(
"tiangolo/uvicorn-gunicorn:python3.7-alpine3.8",
"Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.7",
),
],
)
def test_defaults(image: str, response_text: str):
remove_previous_container(client)
container: Container = client.containers.run(
image,
name=CONTAINER_NAME,
ports={"80": "8000"},
detach=True,
command="/start-reload.sh",
)
time.sleep(1)
verify_container(container, response_text)
container.exec_run(
"sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/main.py"
)
new_response_text = response_text.replace(
"Uvicorn with Gunicorn", "Uvicorn with autoreload"
)
time.sleep(1)
verify_container(container, new_response_text)
container.stop()
container.remove()
70 changes: 70 additions & 0 deletions tests/test_03_reload/test_env_vars_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import time

import docker
import pytest
import requests

from ..utils import CONTAINER_NAME, remove_previous_container, get_logs

client = docker.from_env()


def verify_container(container, response_text):
logs = get_logs(container)
assert "Checking for script in /app/prestart.sh" in logs
assert "Running script /app/prestart.sh" in logs
assert (
"Running inside /app/prestart.sh, you could add migrations to this file" in logs
)
assert "Uvicorn running on http://0.0.0.0:80" in logs
response = requests.get("http://127.0.0.1:8000")
assert response.text == response_text


@pytest.mark.parametrize(
"image,response_text",
[
(
"tiangolo/uvicorn-gunicorn:python3.6",
"Hello world! From Uvicorn with Gunicorn. Using Python 3.6",
),
(
"tiangolo/uvicorn-gunicorn:python3.7",
"Hello world! From Uvicorn with Gunicorn. Using Python 3.7",
),
(
"tiangolo/uvicorn-gunicorn:latest",
"Hello world! From Uvicorn with Gunicorn. Using Python 3.7",
),
(
"tiangolo/uvicorn-gunicorn:python3.6-alpine3.8",
"Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.6",
),
(
"tiangolo/uvicorn-gunicorn:python3.7-alpine3.8",
"Hello world! From Uvicorn with Gunicorn in Alpine. Using Python 3.7",
),
],
)
def test_env_vars_1(image, response_text):
remove_previous_container(client)
container = client.containers.run(
image,
name=CONTAINER_NAME,
environment={"PORT": "8000", "LOG_LEVEL": "debug"},
ports={"8000": "8000"},
detach=True,
command="/start-reload.sh",
)
time.sleep(1)
verify_container(container, response_text)
container.exec_run(
"sed -i 's|Uvicorn with Gunicorn|Uvicorn with autoreload|' /app/main.py"
)
new_response_text = response_text.replace(
"Uvicorn with Gunicorn", "Uvicorn with autoreload"
)
time.sleep(1)
verify_container(container, new_response_text)
container.stop()
container.remove()
Loading