diff --git a/compose/local/fastapi/Dockerfile b/compose/local/fastapi/Dockerfile new file mode 100644 index 0000000..f917e1e --- /dev/null +++ b/compose/local/fastapi/Dockerfile @@ -0,0 +1,43 @@ +FROM python:3.10-slim-buster + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 + +RUN apt-get update \ + # dependencies for building Python packages + && apt-get install -y build-essential \ + # psycopg2 dependencies + && apt-get install -y libpq-dev \ + # Additional dependencies + && apt-get install -y telnet netcat \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +# Requirements are installed here to ensure they will be cached. +COPY ./requirements.txt /requirements.txt +RUN pip install -r /requirements.txt + +COPY ./compose/local/fastapi/entrypoint /entrypoint +RUN sed -i 's/\r$//g' /entrypoint +RUN chmod +x /entrypoint + +COPY ./compose/local/fastapi/start /start-web +RUN sed -i 's/\r$//g' /start-web +RUN chmod +x /start + +COPY ./compose/local/fastapi/celery/worker/start /start-celeryworker +RUN sed -i 's/\r$//g' /start-celeryworker +RUN chmod +x /start-celeryworker + +COPY ./compose/local/fastapi/celery/beat/start /start-celerybeat +RUN sed -i 's/\r$//g' /start-celerybeat +RUN chmod +x /start-celerybeat + +COPY ./compose/local/fastapi/celery/flower/start /start-flower +RUN sed -i 's/\r$//g' /start-flower +RUN chmod +x /start-flower + +WORKDIR /app + +ENTRYPOINT ["/entrypoint"] diff --git a/compose/local/fastapi/celery/beat/start b/compose/local/fastapi/celery/beat/start new file mode 100644 index 0000000..30dc735 --- /dev/null +++ b/compose/local/fastapi/celery/beat/start @@ -0,0 +1,7 @@ +#!/bin/bash + +set -o errexit +set -o nounset + +rm -f './celerybeat.pid' +celery -A main.celery beat -l info diff --git a/compose/local/fastapi/celery/flower/start b/compose/local/fastapi/celery/flower/start new file mode 100644 index 0000000..c8c97e4 --- /dev/null +++ b/compose/local/fastapi/celery/flower/start @@ -0,0 +1,18 @@ +#!/bin/bash + +set -o errexit +set -o nounset + +worker_ready() { + celery -A main.celery inspect ping +} + +until worker_ready; do + >&2 echo 'Celery workers not available' + sleep 1 +done +>&2 echo 'Celery workers is available' + +celery flower \ + --app=main.celery \ + --broker="${CELERY_BROKER_URL}" diff --git a/compose/local/fastapi/celery/worker/start b/compose/local/fastapi/celery/worker/start new file mode 100644 index 0000000..e69de29 diff --git a/compose/local/fastapi/entrypoint b/compose/local/fastapi/entrypoint new file mode 100644 index 0000000..1892104 --- /dev/null +++ b/compose/local/fastapi/entrypoint @@ -0,0 +1,45 @@ +#!/bin/bash + +# if any of the commands in your code fails for any reason, the entire script fails +set -o errexit +# fail exit if one of your pipe command fails +set -o pipefail +# exits if any of your variables is not set +set -o nounset + +postgres_ready() { +python << END +import sys + +import psycopg2 +import urllib.parse as urlparse +import os + +url = urlparse.urlparse(os.environ['DATABASE_URL']) +dbname = url.path[1:] +user = url.username +password = url.password +host = url.hostname +port = url.port + +try: + psycopg2.connect( + dbname=dbname, + user=user, + password=password, + host=host, + port=port + ) +except psycopg2.OperationalError: + sys.exit(-1) +sys.exit(0) + +END +} +until postgres_ready; do + >&2 echo 'Waiting for PostgreSQL to become available...' + sleep 1 +done +>&2 echo 'PostgreSQL is available' + +exec "$@" diff --git a/compose/local/fastapi/start b/compose/local/fastapi/start new file mode 100644 index 0000000..f96db1b --- /dev/null +++ b/compose/local/fastapi/start @@ -0,0 +1,9 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +alembic upgrade head +# Reload because this is a dev setup +uvicorn main:app --reload --reload-dir project --host 0.0.0.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..24ea839 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,80 @@ +version: '3.8' + +services: + web: + build: + context: . + dockerfile: ./compose/local/fastapi/Dockerfile + image: fastapi_celery_example_web + # '/start' is the shell script used to run the service + command: /start + # this volume is used to map the files and folders on the host to the container + # so if we change code on the host, code in the docker container will also be changed + volumes: + - .:/app + ports: + - 8010:8000 + env_file: + - .env/.dev-sample + depends_on: + - redis + - db + + db: + image: postgres:14-alpine + volumes: + - postgres_data:/var/lib/postgresql/data/ + environment: + - POSTGRES_DB=fastapi_celery + - POSTGRES_USER=fastapi_celery + - POSTGRES_PASSWORD=fastapi_celery + + redis: + image: redis:7-alpine + + celery_worker: + build: + context: . + dockerfile: ./compose/local/fastapi/Dockerfile + image: fastapi_celery_example_celery_worker + command: /start-celeryworker + volumes: + - .:/app + env_file: + - .env/.dev-sample + depends_on: + - redis + - db + + celery_beat: + build: + context: . + dockerfile: ./compose/local/fastapi/Dockerfile + image: fastapi_celery_example_celery_beat + command: /start-celerybeat + volumes: + - .:/app + env_file: + - .env/.dev-sample + depends_on: + - redis + - db + + flower: + build: + context: . + dockerfile: ./compose/local/fastapi/Dockerfile + image: fastapi_celery_example_celery_flower + command: /start-flower + volumes: + - .:/app + env_file: + - .env/.dev-sample + ports: + - 5557:5555 + depends_on: + - redis + - db + +volumes: + postgres_data: diff --git a/requirements.in b/requirements.in index 06f1a64..023e58b 100644 --- a/requirements.in +++ b/requirements.in @@ -2,6 +2,7 @@ alembic==1.8.1 celery==5.2.7 fastapi==0.79.0 flower==1.2.0 +psycopg2-binary==2.9.3 redis==4.3.4 SQLAlchemy==1.4.40 uvicorn[standard]==0.18.2 diff --git a/requirements.txt b/requirements.txt index 43e11b8..5ca8432 100644 --- a/requirements.txt +++ b/requirements.txt @@ -65,6 +65,8 @@ prometheus-client==0.14.1 # via flower prompt-toolkit==3.0.31 # via click-repl +psycopg2-binary==2.9.3 + # via -r requirements.in pydantic==1.10.2 # via fastapi pyparsing==3.0.9