Skip to content

Commit

Permalink
✨ Add extra configs and docs (#38)
Browse files Browse the repository at this point in the history
* ✨ Implement extra config env vars

* ✅ Add tests for extra config env vars

* ✨ Add support for extra configs, error log, acess log, etc

* ✅ Add tests for extra configs

* 📝 Add docs for extra configs

* ✨ Add support for MAX_WORKERS

* ✅ Add tests for max workers

* 📝 Add docs for MAX_WORKERS

* ♻️ Move worker_class back to start.sh to prevent current applications from breaking

* ✅ Update tests

* ✅ Fix tests
  • Loading branch information
tiangolo authored Apr 27, 2020
1 parent 8a404f8 commit 2daa3e3
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 11 deletions.
126 changes: 126 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ You can set it like:
docker run -d -p 80:80 -e GUNICORN_CONF="/app/custom_gunicorn_conf.py" myimage
```

You can use the [config file](https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/master/docker-images/gunicorn_conf.py) from this image as a starting point for yours.

#### `WORKERS_PER_CORE`

This image will check how many CPU cores are available in the current server running your container.
Expand Down Expand Up @@ -204,6 +206,24 @@ In a server with 8 CPU cores, this would make it start only 4 worker processes.

**Note**: By default, if `WORKERS_PER_CORE` is `1` and the server has only 1 CPU core, instead of starting 1 single worker, it will start 2. This is to avoid bad performance and blocking applications (server application) on small machines (server machine/cloud/etc). This can be overridden using `WEB_CONCURRENCY`.

#### `MAX_WORKERS`

Set the maximum number of workers to use.

You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum.

This can be useful, for example, if each worker uses a database connection and your database has a maximum limit of open connections.

By default it's not set, meaning that it's unlimited.

You can set it like:

```bash
docker run -d -p 80:80 -e MAX_WORKERS="24" myimage
```

This would make the image start at most 24 workers, independent of how many CPU cores are available in the server.

#### `WEB_CONCURRENCY`

Override the automatic definition of number of workers.
Expand Down Expand Up @@ -288,6 +308,112 @@ You can set it like:
docker run -d -p 80:8080 -e LOG_LEVEL="warning" myimage
```

#### `WORKER_CLASS`

The class to be used by Gunicorn for the workers.

By default, set to `uvicorn.workers.UvicornWorker`.

The fact that it uses Uvicorn is what allows using ASGI applications like FastAPI and Starlette, and that is also what provides the maximum performance.

You probably shouldn't change it.

But if for some reason you need to use the alternative Uvicorn worker: `uvicorn.workers.UvicornH11Worker` you can set it with this environment variable.

You can set it like:

```bash
docker run -d -p 80:8080 -e WORKER_CLASS="uvicorn.workers.UvicornH11Worker" myimage
```

#### `TIMEOUT`

Workers silent for more than this many seconds are killed and restarted.

Read more about it in the [Gunicorn docs: timeout](https://docs.gunicorn.org/en/stable/settings.html#timeout).

By default, set to `120`.

Notice that Uvicorn and ASGI frameworks like FastAPI and Starlette are async, not sync. So it's probably safe to have higher timeouts than for sync workers.

You can set it like:

```bash
docker run -d -p 80:8080 -e TIMEOUT="20" myimage
```

#### `KEEP_ALIVE`

The number of seconds to wait for requests on a Keep-Alive connection.

Read more about it in the [Gunicorn docs: keepalive](https://docs.gunicorn.org/en/stable/settings.html#keepalive).

By default, set to `2`.

You can set it like:

```bash
docker run -d -p 80:8080 -e KEEP_ALIVE="20" myimage
```

#### `GRACEFUL_TIMEOUT`

Timeout for graceful workers restart.

Read more about it in the [Gunicorn docs: graceful-timeout](https://docs.gunicorn.org/en/stable/settings.html#graceful-timeout).

By default, set to `120`.

You can set it like:

```bash
docker run -d -p 80:8080 -e GRACEFUL_TIMEOUT="20" myimage
```

#### `ACCESS_LOG`

The access log file to write to.

By default `"-"`, which means stdout (print in the Docker logs).

If you want to disable `ACCESS_LOG`, set it to an empty value.

For example, you could disable it with:

```bash
docker run -d -p 80:8080 -e ACCESS_LOG= myimage
```

#### `ERROR_LOG`

The error log file to write to.

By default `"-"`, which means stderr (print in the Docker logs).

If you want to disable `ERROR_LOG`, set it to an empty value.

For example, you could disable it with:

```bash
docker run -d -p 80:8080 -e ERROR_LOG= myimage
```

#### `GUNICORN_CMD_ARGS`

Any additional command line settings for Gunicorn can be passed in the `GUNICORN_CMD_ARGS` environment variable.

Read more about it in the [Gunicorn docs: Settings](https://docs.gunicorn.org/en/stable/settings.html#settings).

These settings will have precedence over the other environment variables and any Gunicorn config file.

For example, if you have a custom TLS/SSL certificate that you want to use, you could copy them to the Docker image or mount them in the container, and set [`--keyfile` and `--certfile`](http://docs.gunicorn.org/en/latest/settings.html#ssl) to the location of the files, for example:

```bash
docker run -d -p 80:8080 -e GUNICORN_CMD_ARGS="--keyfile=/secrets/key.pem --certfile=/secrets/cert.pem" -e PORT=443 myimage
```

**Note**: instead of handling TLS/SSL yourself and configuring it in the container, it's recommended to use a "TLS Termination Proxy" like [Traefik](https://docs.traefik.io/). You can read more about it in the [FastAPI documentation about HTTPS](https://fastapi.tiangolo.com/deployment/#https).

#### `PRE_START_PATH`

The path where to find the pre-start script.
Expand Down
28 changes: 26 additions & 2 deletions docker-images/gunicorn_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import os

workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)

host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "80")
bind_env = os.getenv("BIND", None)
Expand All @@ -21,22 +26,41 @@
assert web_concurrency > 0
else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")

# Gunicorn config variables
loglevel = use_loglevel
workers = web_concurrency
bind = use_bind
keepalive = 120
errorlog = "-"
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)


# For debugging and testing
log_data = {
"loglevel": loglevel,
"workers": workers,
"bind": bind,
"graceful_timeout": graceful_timeout,
"timeout": timeout,
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
}
Expand Down
3 changes: 2 additions & 1 deletion docker-images/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ else
DEFAULT_GUNICORN_CONF=/gunicorn_conf.py
fi
export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF}
export WORKER_CLASS=${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"}

# If there's a prestart.sh script in the /app directory or other path specified, run it before starting
PRE_START_PATH=${PRE_START_PATH:-/app/prestart.sh}
Expand All @@ -30,4 +31,4 @@ else
fi

# Start Gunicorn
exec gunicorn -k uvicorn.workers.UvicornWorker -c "$GUNICORN_CONF" "$APP_MODULE"
exec gunicorn -k "$WORKER_CLASS" -c "$GUNICORN_CONF" "$APP_MODULE"
13 changes: 11 additions & 2 deletions tests/test_01_main/test_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,30 @@


def verify_container(container: DockerClient, response_text: str) -> None:
response = requests.get("http://127.0.0.1:8000")
assert response.text == response_text
config_data = get_config(container)
assert config_data["workers_per_core"] == 1
assert config_data["use_max_workers"] is None
assert config_data["host"] == "0.0.0.0"
assert config_data["port"] == "80"
assert config_data["loglevel"] == "info"
assert config_data["workers"] >= 2
assert config_data["bind"] == "0.0.0.0:80"
assert config_data["graceful_timeout"] == 120
assert config_data["timeout"] == 120
assert config_data["keepalive"] == 5
assert config_data["errorlog"] == "-"
assert config_data["accesslog"] == "-"
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
)
response = requests.get("http://127.0.0.1:8000")
assert response.text == response_text
assert '"GET / HTTP/1.1" 200' in logs
assert "[INFO] Application startup complete." in logs
assert "Using worker: uvicorn.workers.UvicornWorker" in logs


def test_defaults() -> None:
Expand Down
22 changes: 19 additions & 3 deletions tests/test_01_main/test_env_vars_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@


def verify_container(container: DockerClient, response_text: str) -> None:
response = requests.get("http://127.0.0.1:8000")
assert response.text == response_text
config_data = get_config(container)
assert config_data["workers_per_core"] == 2
assert config_data["host"] == "0.0.0.0"
assert config_data["port"] == "8000"
assert config_data["loglevel"] == "warning"
assert config_data["bind"] == "0.0.0.0:8000"
assert config_data["graceful_timeout"] == 20
assert config_data["timeout"] == 20
assert config_data["keepalive"] == 20
assert config_data["errorlog"] is None
assert config_data["accesslog"] is None
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
)
response = requests.get("http://127.0.0.1:8000")
assert response.text == response_text
assert '"GET / HTTP/1.1" 200' not in logs
assert "[INFO] Application startup complete." not in logs


def test_env_vars_1() -> None:
Expand All @@ -42,7 +49,16 @@ def test_env_vars_1() -> None:
container = client.containers.run(
image,
name=CONTAINER_NAME,
environment={"WORKERS_PER_CORE": 2, "PORT": "8000", "LOG_LEVEL": "warning"},
environment={
"WORKERS_PER_CORE": 2,
"PORT": "8000",
"LOG_LEVEL": "warning",
"GRACEFUL_TIMEOUT": "20",
"TIMEOUT": "20",
"KEEP_ALIVE": "20",
"ACCESS_LOG": "",
"ERROR_LOG": "",
},
ports={"8000": "8000"},
detach=True,
)
Expand Down
17 changes: 15 additions & 2 deletions tests/test_01_main/test_env_vars_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ def verify_container(container: Container) -> None:
assert len(process_names) == 2 # Manager + worker
assert config_data["host"] == "127.0.0.1"
assert config_data["port"] == "80"
assert config_data["loglevel"] == "info"
assert config_data["loglevel"] == "warning"
assert config_data["bind"] == "127.0.0.1:80"
assert config_data["graceful_timeout"] == 120
assert config_data["timeout"] == 120
assert config_data["keepalive"] == 5
assert config_data["errorlog"] == "-"
assert config_data["accesslog"] == "-"
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 "loglevel: debug" in logs
assert "Using worker: uvicorn.workers.UvicornH11Worker" in logs


def test_env_vars_2() -> None:
Expand All @@ -40,7 +47,13 @@ def test_env_vars_2() -> None:
container = client.containers.run(
image,
name=CONTAINER_NAME,
environment={"WEB_CONCURRENCY": 1, "HOST": "127.0.0.1"},
environment={
"WEB_CONCURRENCY": 1,
"HOST": "127.0.0.1",
"LOG_LEVEL": "warning",
"GUNICORN_CMD_ARGS": "--log-level debug",
"WORKER_CLASS": "uvicorn.workers.UvicornH11Worker",
},
ports={"80": "8000"},
detach=True,
)
Expand Down
13 changes: 12 additions & 1 deletion tests/test_01_main/test_env_vars_3.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CONTAINER_NAME,
get_config,
get_logs,
get_process_names,
get_response_text1,
remove_previous_container,
)
Expand All @@ -18,9 +19,14 @@

def verify_container(container: DockerClient, response_text: str) -> None:
config_data = get_config(container)
process_names = get_process_names(container)
config_data = get_config(container)
assert config_data["workers"] == 1
assert len(process_names) == 2 # Manager + worker
assert config_data["host"] == "127.0.0.1"
assert config_data["port"] == "9000"
assert config_data["bind"] == "0.0.0.0:8080"
assert config_data["use_max_workers"] == 1
logs = get_logs(container)
assert "Checking for script in /app/prestart.sh" in logs
assert "Running script /app/prestart.sh" in logs
Expand All @@ -40,7 +46,12 @@ def test_env_bind() -> None:
container = client.containers.run(
image,
name=CONTAINER_NAME,
environment={"BIND": "0.0.0.0:8080", "HOST": "127.0.0.1", "PORT": "9000"},
environment={
"BIND": "0.0.0.0:8080",
"HOST": "127.0.0.1",
"PORT": "9000",
"MAX_WORKERS": "1",
},
ports={"8080": "8000"},
detach=True,
)
Expand Down

0 comments on commit 2daa3e3

Please sign in to comment.