├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── image-build.yml │ └── versionator.yml ├── LICENSE ├── README.md ├── assets ├── main.py └── start.sh ├── dockerfile └── templates ├── description.md ├── documentation.md └── quick_start.md /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | ** 3 | 4 | # Allow files and directories 5 | !/assets/* 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/image-build.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Gunicorn Uvicorn Image Builder 3 | 4 | # Publish on new pushed, and build on Monday Morning (UTC) regardless. 5 | on: 6 | push: 7 | branches: 8 | - 'main' 9 | paths-ignore: 10 | - '**/README.md' 11 | - 'templates/**' 12 | schedule: 13 | - cron: '10 0 * * *' 14 | 15 | 16 | jobs: 17 | Uvicorn-Builder: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python_versions: ["3.9", "3.10", "3.11", "3.12", "3.13"] 23 | package_versions: ["0.31.1", "0.32.0", "0.32.1", "0.33.0", "0.34.0"] 24 | target_base: ["full", "slim"] 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: "Create and Push Image" 30 | uses: multi-py/action-python-image-builder@v1 31 | timeout-minutes: 45 32 | with: 33 | package: "uvicorn[standard]" 34 | package_latest_version: "0.34.0" 35 | maintainer: "Robert Hafner " 36 | python_version: ${{ matrix.python_versions }} 37 | package_version: ${{ matrix.package_versions }} 38 | target_base: ${{ matrix.target_base }} 39 | registry_password: ${{ secrets.GITHUB_TOKEN }} 40 | dockerfile: "${{ github.workspace }}/dockerfile" 41 | docker_build_path: "${{ github.workspace }}/" 42 | -------------------------------------------------------------------------------- /.github/workflows/versionator.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Uvicorn Version Updater 3 | 4 | # Every 30 minutes check for a new version of the package. 5 | on: 6 | push: 7 | branches: 8 | - 'main' 9 | schedule: 10 | - cron: '10,50 * * * *' 11 | 12 | jobs: 13 | Version-Updater: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | token: ${{ secrets.WORKFLOW_GITHUB_TOKEN }} 20 | 21 | - name: "Update Image Build Workflow" 22 | uses: multi-py/action-python-versionator@v1 23 | with: 24 | package: "uvicorn" 25 | git_name: "Robert Hafner" 26 | git_email: "tedivm@tedivm.com" 27 | action_path: ${{ github.workspace }}/.github/workflows/image-build.yml 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Robert Hafner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-gunicorn-uvicorn 2 | 3 | 4 | A multiarchitecture container image for running Python with Gunicorn and Uvicorn. 5 | 6 | For this container the latest version of gunicorn is always used, and the tags represent the uvicorn version. 7 | 8 | 9 | * [python-gunicorn-uvicorn](#python-gunicorn-uvicorn) 10 | * [Benefits](#benefits) 11 | * [Multi Architecture Builds](#multi-architecture-builds) 12 | * [Small Images via Multi Stage Builds](#small-images-via-multi-stage-builds) 13 | * [No Rate Limits](#no-rate-limits) 14 | * [Rapid Building of New Versions](#rapid-building-of-new-versions) 15 | * [Regular Updates](#regular-updates) 16 | * [How To](#how-to) 17 | * [Using the Full Image](#using-the-full-image) 18 | * [Using the Slim Image](#using-the-slim-image) 19 | * [Copy Just the Packages](#copy-just-the-packages) 20 | * [Add Your App](#add-your-app) 21 | * [PreStart Script](#prestart-script) 22 | * [Environmental Variables](#environmental-variables) 23 | * [PORT](#port) 24 | * [WORKERS](#workers) 25 | * [LOG_LEVEL](#log_level) 26 | * [MODULE_NAME](#module_name) 27 | * [VARIABLE_NAME](#variable_name) 28 | * [APP_MODULE](#app_module) 29 | * [PRE_START_PATH](#pre_start_path) 30 | * [RELOAD](#reload) 31 | * [GUNICORN_EXTRA_FLAGS](#gunicorn_extra_flags) 32 | * [UVICORN_EXTRA_FLAGS](#uvicorn_extra_flags) 33 | * [Python Versions](#python-versions) 34 | * [Image Variants](#image-variants) 35 | * [Full](#full) 36 | * [Slim](#slim) 37 | * [Architectures](#architectures) 38 | * [Sponsorship](#sponsorship) 39 | * [Tags](#tags) 40 | * [Older Tags](#older-tags) 41 | 42 | 43 | ## Benefits 44 | 45 | ### Multi Architecture Builds 46 | 47 | Every tag in this repository supports these architectures: 48 | 49 | * linux/amd64 50 | * linux/arm64 51 | * linux/arm/v7 52 | 53 | 54 | ### Small Images via Multi Stage Builds 55 | 56 | All libraries are compiled in one image before being moved into the final published image. This keeps all of the build tools out of the published container layers. 57 | 58 | ### No Rate Limits 59 | 60 | This project uses the Github Container Registry to store images, which have no rate limiting on pulls (unlike Docker Hub). 61 | 62 | ### Rapid Building of New Versions 63 | 64 | Within 30 minutes of a new release to uvicorn on PyPI builds will kick off for new containers. This means new versions can be used in hours, not days. 65 | 66 | ### Regular Updates 67 | 68 | Containers are rebuilt weekly in order to take on the security patches from upstream containers. 69 | 70 | ## How To 71 | 72 | ### Using the Full Image 73 | The Full Images use the base Python Docker images as their parent. These images are based off of Ubuntu and contain a variety of build tools. 74 | 75 | To pull the latest full version: 76 | 77 | ```bash 78 | docker pull ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-LATEST 79 | ``` 80 | 81 | To include it in the dockerfile instead: 82 | 83 | ```dockerfile 84 | FROM ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-LATEST 85 | ``` 86 | 87 | ### Using the Slim Image 88 | 89 | The Slim Images use the base Python Slim Docker images as their parent. These images are very similar to the Full images, but without the build tools. These images are much smaller than their counter parts but are more difficult to compile wheels on. 90 | 91 | To pull the latest slim version: 92 | 93 | ```bash 94 | docker pull ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-slim-LATEST 95 | ``` 96 | 97 | To include it in the dockerfile instead: 98 | 99 | ```dockerfile 100 | FROM ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-slim-LATEST 101 | ``` 102 | 103 | 104 | 105 | 106 | 107 | ### Copy Just the Packages 108 | It's also possible to copy just the Python packages themselves. This is particularly useful when you want to use the precompiled libraries from multiple containers. 109 | 110 | ```dockerfile 111 | FROM python:3.12 112 | 113 | COPY --from=ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-slim-LATEST /usr/local/lib/python3.12/site-packages/* /usr/local/lib/python3.12/site-packages/ 114 | ``` 115 | 116 | ### Add Your App 117 | 118 | By default the startup script checks for the following packages and uses the first one it can find- 119 | 120 | * `/app/app/main.py` 121 | * `/app/main.py` 122 | 123 | If you are using pip to install dependencies your dockerfile could look like this- 124 | 125 | ```dockerfile 126 | FROM ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-0.34.0 127 | 128 | COPY requirements /requirements 129 | RUN pip install --no-cache-dir -r /requirements 130 | COPY ./app app 131 | ``` 132 | 133 | 134 | ### PreStart Script 135 | 136 | When the container is launched it will run the script at `/app/prestart.sh` before starting the uvicorn service. This is an ideal place to put things like database migrations. 137 | 138 | ## Environmental Variables 139 | 140 | ### `PORT` 141 | 142 | The port that the application inside of the container will listen on. This is different from the host port that gets mapped to the container. 143 | 144 | 145 | ### `WORKERS` 146 | 147 | The number of workers to launch. A good starting point is 4 processes per core (defaults to 4). 148 | 149 | 150 | ### `LOG_LEVEL` 151 | 152 | The uvicorn log level. Must be one of the following: 153 | 154 | * `critical` 155 | * `error` 156 | * `warning` 157 | * `info` 158 | * `debug` 159 | * `trace` 160 | 161 | 162 | ### `MODULE_NAME` 163 | 164 | The python module that uvicorn will import. This value is used to generate the APP_MODULE value. 165 | 166 | 167 | ### `VARIABLE_NAME` 168 | 169 | The python variable containing the ASGI application inside of the module that uvicorn imports. This value is used to generate the APP_MODULE value. 170 | 171 | 172 | ### `APP_MODULE` 173 | 174 | The python module and variable that is passed to uvicorn. When used the `VARIABLE_NAME` and `MODULE_NAME` environmental variables are ignored. 175 | 176 | 177 | ### `PRE_START_PATH` 178 | 179 | Where to find the prestart script. 180 | 181 | 182 | ### `RELOAD` 183 | 184 | When this is set to the string `true` uvicorn is launched in reload mode. If any files change uvicorn will reload the modules again, allowing for quick debugging. This comes at a performance cost, however, and should not be enabled on production machines. 185 | 186 | ### `GUNICORN_EXTRA_FLAGS` 187 | 188 | This variable can be used to pass extra flags to the `gunicorn` command on launch. It's value is added directly to the command that is called, and has to be formatted appropriately for the command line. 189 | 190 | 191 | ### `UVICORN_EXTRA_FLAGS` 192 | 193 | This variable can be used to pass extra flags to the `uvicorn` command on launch. It's value is added directly to the command that is called, and has to be formatted appropriately for the command line. 194 | 195 | 196 | ## Python Versions 197 | 198 | This project actively supports these Python versions: 199 | 200 | * 3.12 201 | * 3.11 202 | * 3.10 203 | * 3.9 204 | * 3.8 205 | 206 | 207 | ## Image Variants 208 | 209 | Like the upstream Python containers themselves a variety of image variants are supported. 210 | 211 | 212 | ### Full 213 | 214 | The default container type, and if you're not sure what container to use start here. It has a variety of libraries and build tools installed, making it easy to extend. 215 | 216 | 217 | 218 | ### Slim 219 | 220 | This container is similar to Full but with far less libraries and tools installed by default. If yo're looking for the tiniest possible image with the most stability this is your best bet. 221 | 222 | 223 | 224 | 225 | 226 | ## Architectures 227 | 228 | Every tag in this repository supports these architectures: 229 | 230 | * linux/amd64 231 | * linux/arm64 232 | * linux/arm/v7 233 | 234 | 235 | ## Sponsorship 236 | 237 | If you get use out of these containers please consider sponsoring me using Github! 238 |
239 | 240 | [![Github Sponsorship](https://raw.githubusercontent.com/mechPenSketch/mechPenSketch/master/img/github_sponsor_btn.svg)](https://github.com/sponsors/tedivm) 241 | 242 |
243 | 244 | ## Tags 245 | * Recommended Image: `ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-0.34.0` 246 | * Slim Image: `ghcr.io/multi-py/python-gunicorn-uvicorn:py3.12-slim-0.34.0` 247 | 248 | Tags are based on the package version, python version, and the upstream container the container is based on. 249 | 250 | | uvicorn Version | Python Version | Full Container | Slim Container | 251 | |-----------------------|----------------|----------------|----------------| 252 | | latest | 3.12 | py3.12-latest | py3.12-slim-latest | 253 | | latest | 3.11 | py3.11-latest | py3.11-slim-latest | 254 | | latest | 3.10 | py3.10-latest | py3.10-slim-latest | 255 | | latest | 3.9 | py3.9-latest | py3.9-slim-latest | 256 | | latest | 3.8 | py3.8-latest | py3.8-slim-latest | 257 | | 0.34.0 | 3.12 | py3.12-0.34.0 | py3.12-slim-0.34.0 | 258 | | 0.34.0 | 3.11 | py3.11-0.34.0 | py3.11-slim-0.34.0 | 259 | | 0.34.0 | 3.10 | py3.10-0.34.0 | py3.10-slim-0.34.0 | 260 | | 0.34.0 | 3.9 | py3.9-0.34.0 | py3.9-slim-0.34.0 | 261 | | 0.34.0 | 3.8 | py3.8-0.34.0 | py3.8-slim-0.34.0 | 262 | | 0.33.0 | 3.12 | py3.12-0.33.0 | py3.12-slim-0.33.0 | 263 | | 0.33.0 | 3.11 | py3.11-0.33.0 | py3.11-slim-0.33.0 | 264 | | 0.33.0 | 3.10 | py3.10-0.33.0 | py3.10-slim-0.33.0 | 265 | | 0.33.0 | 3.9 | py3.9-0.33.0 | py3.9-slim-0.33.0 | 266 | | 0.33.0 | 3.8 | py3.8-0.33.0 | py3.8-slim-0.33.0 | 267 | | 0.32.1 | 3.12 | py3.12-0.32.1 | py3.12-slim-0.32.1 | 268 | | 0.32.1 | 3.11 | py3.11-0.32.1 | py3.11-slim-0.32.1 | 269 | | 0.32.1 | 3.10 | py3.10-0.32.1 | py3.10-slim-0.32.1 | 270 | | 0.32.1 | 3.9 | py3.9-0.32.1 | py3.9-slim-0.32.1 | 271 | | 0.32.1 | 3.8 | py3.8-0.32.1 | py3.8-slim-0.32.1 | 272 | | 0.32.0 | 3.12 | py3.12-0.32.0 | py3.12-slim-0.32.0 | 273 | | 0.32.0 | 3.11 | py3.11-0.32.0 | py3.11-slim-0.32.0 | 274 | | 0.32.0 | 3.10 | py3.10-0.32.0 | py3.10-slim-0.32.0 | 275 | | 0.32.0 | 3.9 | py3.9-0.32.0 | py3.9-slim-0.32.0 | 276 | | 0.32.0 | 3.8 | py3.8-0.32.0 | py3.8-slim-0.32.0 | 277 | | 0.31.1 | 3.12 | py3.12-0.31.1 | py3.12-slim-0.31.1 | 278 | | 0.31.1 | 3.11 | py3.11-0.31.1 | py3.11-slim-0.31.1 | 279 | | 0.31.1 | 3.10 | py3.10-0.31.1 | py3.10-slim-0.31.1 | 280 | | 0.31.1 | 3.9 | py3.9-0.31.1 | py3.9-slim-0.31.1 | 281 | | 0.31.1 | 3.8 | py3.8-0.31.1 | py3.8-slim-0.31.1 | 282 | 283 | 284 | ### Older Tags 285 | 286 | Older tags are left for historic purposes but do not receive updates. A full list of tags can be found on the package's [registry page](https://github.com/multi-py/python-gunicorn-uvicorn/pkgs/container/python-gunicorn-uvicorn). 287 | 288 | -------------------------------------------------------------------------------- /assets/main.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This is an example "pure python" uvicorn app. 4 | # 5 | 6 | async def app(scope, receive, send): 7 | assert scope['type'] == 'http' 8 | 9 | await send({ 10 | 'type': 'http.response.start', 11 | 'status': 200, 12 | 'headers': [ 13 | [b'content-type', b'text/plain'], 14 | ], 15 | }) 16 | await send({ 17 | 'type': 'http.response.body', 18 | 'body': b'Hello, world!', 19 | }) 20 | -------------------------------------------------------------------------------- /assets/start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -e 3 | 4 | # 5 | # The follow block comes from tiangolo/uvicorn-gunicorn-docker 6 | # MIT License: https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/master/LICENSE 7 | # 8 | 9 | if [ -f /app/app/main.py ]; then 10 | DEFAULT_MODULE_NAME=app.main 11 | elif [ -f /app/main.py ]; then 12 | DEFAULT_MODULE_NAME=main 13 | fi 14 | MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} 15 | VARIABLE_NAME=${VARIABLE_NAME:-app} 16 | export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} 17 | 18 | # If there's a prestart.sh script in the /app directory or other path specified, run it before starting 19 | PRE_START_PATH=${PRE_START_PATH:-/app/prestart.sh} 20 | echo "Checking for script in $PRE_START_PATH" 21 | if [ -f $PRE_START_PATH ]; then 22 | echo "Running script $PRE_START_PATH" 23 | . "$PRE_START_PATH" 24 | else 25 | echo "There is no script $PRE_START_PATH" 26 | fi 27 | 28 | # 29 | # End of tiangolo/uvicorn-gunicorn-docker block 30 | # 31 | 32 | if [[ $RELOAD == "true" ]]; then 33 | exec python -m gunicorn "$APP_MODULE" $GUNICORN_EXTRA_FLAGS -k uvicorn.workers.UvicornWorker $UVICORN_EXTRA_FLAGS -k gevent -w ${WORKERS:-4} -b ${HOST:-0.0.0.0}:${PORT:-80} --log-level "${LOG_LEVEL:-info}" --reload 34 | else 35 | exec python -m gunicorn "$APP_MODULE" $GUNICORN_EXTRA_FLAGS -k uvicorn.workers.UvicornWorker $UVICORN_EXTRA_FLAGS -k gevent -w ${WORKERS:-4} -b ${HOST:-0.0.0.0}:${PORT:-80} --log-level "${LOG_LEVEL:-info}" 36 | fi 37 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | ARG python_version=3.9 2 | ARG publish_target=$python_version 3 | ARG package_version 4 | FROM ghcr.io/multi-py/python-uvicorn:py$publish_target-$package_version as uvicorn 5 | 6 | ARG python_version 7 | ARG publish_target 8 | ARG package_version 9 | FROM ghcr.io/multi-py/python-gunicorn:py$publish_target-LATEST 10 | 11 | # Add args to container scope. 12 | ARG publish_target 13 | ARG python_version 14 | ARG package 15 | ARG package_version 16 | ARG maintainer="" 17 | ARG TARGETPLATFORM="" 18 | LABEL python=$python_version 19 | LABEL package=$package 20 | LABEL maintainer=$maintainer 21 | LABEL org.opencontainers.image.description="python:$publish_target $package:$package_version $TARGETPLATFORM" 22 | 23 | 24 | COPY --from=uvicorn /usr/local/lib/python${python_version}/site-packages/* /usr/local/lib/python${python_version}/site-packages/ 25 | 26 | # Startup Script 27 | COPY ./assets/start.sh /start.sh 28 | RUN chmod +x /start.sh 29 | 30 | # Example application so container "works" when run directly. 31 | COPY ./assets/main.py /app/main.py 32 | WORKDIR /app/ 33 | 34 | ENV PYTHONPATH=/app 35 | 36 | CMD ["/start.sh"] 37 | -------------------------------------------------------------------------------- /templates/description.md: -------------------------------------------------------------------------------- 1 | 2 | A multiarchitecture container image for running Python with Gunicorn and Uvicorn. 3 | 4 | For this container the latest version of gunicorn is always used, and the tags represent the uvicorn version. 5 | -------------------------------------------------------------------------------- /templates/documentation.md: -------------------------------------------------------------------------------- 1 | ## Environmental Variables 2 | 3 | ### `PORT` 4 | 5 | The port that the application inside of the container will listen on. This is different from the host port that gets mapped to the container. 6 | 7 | 8 | ### `WORKERS` 9 | 10 | The number of workers to launch. A good starting point is 4 processes per core (defaults to 4). 11 | 12 | 13 | ### `LOG_LEVEL` 14 | 15 | The uvicorn log level. Must be one of the following: 16 | 17 | * `critical` 18 | * `error` 19 | * `warning` 20 | * `info` 21 | * `debug` 22 | * `trace` 23 | 24 | 25 | ### `MODULE_NAME` 26 | 27 | The python module that uvicorn will import. This value is used to generate the APP_MODULE value. 28 | 29 | 30 | ### `VARIABLE_NAME` 31 | 32 | The python variable containing the ASGI application inside of the module that uvicorn imports. This value is used to generate the APP_MODULE value. 33 | 34 | 35 | ### `APP_MODULE` 36 | 37 | The python module and variable that is passed to uvicorn. When used the `VARIABLE_NAME` and `MODULE_NAME` environmental variables are ignored. 38 | 39 | 40 | ### `PRE_START_PATH` 41 | 42 | Where to find the prestart script. 43 | 44 | 45 | ### `RELOAD` 46 | 47 | When this is set to the string `true` uvicorn is launched in reload mode. If any files change uvicorn will reload the modules again, allowing for quick debugging. This comes at a performance cost, however, and should not be enabled on production machines. 48 | 49 | ### `GUNICORN_EXTRA_FLAGS` 50 | 51 | This variable can be used to pass extra flags to the `gunicorn` command on launch. It's value is added directly to the command that is called, and has to be formatted appropriately for the command line. 52 | 53 | 54 | ### `UVICORN_EXTRA_FLAGS` 55 | 56 | This variable can be used to pass extra flags to the `uvicorn` command on launch. It's value is added directly to the command that is called, and has to be formatted appropriately for the command line. 57 | 58 | -------------------------------------------------------------------------------- /templates/quick_start.md: -------------------------------------------------------------------------------- 1 | {% set short_repository = repository.split("/")[1] -%} 2 | 3 | ### Add Your App 4 | 5 | By default the startup script checks for the following packages and uses the first one it can find- 6 | 7 | * `/app/app/main.py` 8 | * `/app/main.py` 9 | 10 | If you are using pip to install dependencies your dockerfile could look like this- 11 | 12 | ```dockerfile 13 | FROM ghcr.io/{{ organization }}/{{ short_repository }}:py{{ python_versions|last }}-{{ package_versions|last }} 14 | 15 | COPY requirements /requirements 16 | RUN pip install --no-cache-dir -r /requirements 17 | COPY ./app app 18 | ``` 19 | 20 | 21 | ### PreStart Script 22 | 23 | When the container is launched it will run the script at `/app/prestart.sh` before starting the uvicorn service. This is an ideal place to put things like database migrations. 24 | --------------------------------------------------------------------------------