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

Docker / Docker-compose improvements #1314

Merged
merged 4 commits into from
Jul 26, 2024
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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
12
20
51 changes: 31 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# includes compilers and build tooling to create the environment
FROM python:3.11-slim-bookworm AS backend-build

RUN apt-get update && apt-get install -y --no-install-recommends \
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
pkg-config \
build-essential \
git \
Expand All @@ -24,21 +24,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
RUN mkdir /app/src

# Ensure we use the latest version of pip
RUN pip install pip>=24 setuptools -U
# Use uv to install dependencies
RUN pip install uv -U
COPY ./requirements /app/requirements
RUN pip install -r requirements/production.txt


# Stage 2 - Install frontend deps and build assets
FROM node:20-buster AS frontend-build
RUN uv pip install --system -r requirements/production.txt

RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*

# Stage 2 - Install frontend deps and build assets
FROM node:20-bookworm-slim AS frontend-build
joeribekker marked this conversation as resolved.
Show resolved Hide resolved

WORKDIR /app

# copy configuration/build files
Expand All @@ -54,19 +53,17 @@ COPY ./src /app/src
# build frontend
RUN npm run build


# Stage 3 - Build docker image suitable for production

FROM python:3.11-slim-bookworm

# Stage 3.1 - Set up the needed production dependencies
# Note: mime-support becomes media-types in Debian Bullseye (required for correctly serving mime-types for images)
# Also install the dependencies for GeoDjango

RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
curl \
procps \
vim \
nano \
mime-support \
postgresql-client \
libgdal32 \
libgeos-c1v5 \
Expand All @@ -75,8 +72,8 @@ RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-reco
libxmlsec1-openssl \
libgdk-pixbuf2.0-0 \
libffi-dev \
gettext \
shared-mime-info \
mime-support \
# weasyprint deps (https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#debian-11)
libpango-1.0-0 \
libpangoft2-1.0-0 \
Expand All @@ -86,15 +83,19 @@ WORKDIR /app
COPY ./bin/docker_start.sh /start.sh
COPY ./bin/wait_for_db.sh /wait_for_db.sh
COPY ./bin/celery_worker.sh /celery_worker.sh
COPY ./bin/check_celery_worker_liveness.py ./bin/
COPY ./bin/celery_beat.sh /celery_beat.sh
COPY ./bin/celery_monitor.sh /celery_monitor.sh
COPY ./bin/setup_configuration.sh /setup_configuration.sh
RUN mkdir /app/log
RUN mkdir /app/media
RUN mkdir /app/log /app/media /app/private_media /app/tmp
COPY ./bin/check_celery_worker_liveness.py ./bin/

# prevent writing to the container layer, which would degrade performance.
# This also serves as a hint for the intended volumes.
VOLUME ["/app/log", "/app/media", "/app/private_media"]

# copy backend build deps
COPY --from=backend-build /usr/local/lib/python3.11 /usr/local/lib/python3.11
COPY --from=backend-build /usr/local/bin/uwsgi /usr/local/bin/uwsgi
COPY --from=backend-build /app/src/ /app/src/
joeribekker marked this conversation as resolved.
Show resolved Hide resolved
COPY --from=backend-build /usr/local/bin/celery /usr/local/bin/celery

# copy frontend build statics
Expand All @@ -109,16 +110,26 @@ RUN chown -R maykin /app
# drop privileges
USER maykin

ARG COMMIT_HASH
ARG RELEASE COMMIT_HASH
ENV GIT_SHA=${COMMIT_HASH}
ENV RELEASE=${RELEASE}

ENV DJANGO_SETTINGS_MODULE=open_inwoner.conf.docker

ENV DIGID_MOCK=True
ENV EHERKENNING_MOCK=True

ARG SECRET_KEY=dummy

# Run collectstatic, so the result is already included in the image
RUN python src/manage.py collectstatic --noinput
LABEL org.label-schema.vcs-ref=$COMMIT_HASH \
org.label-schema.vcs-url="https://github.com/maykinmedia/open-inwoner" \
org.label-schema.version=$RELEASE \
org.label-schema.name="Open Inwoner"

# Run collectstatic and compilemessages, so the result is already included in
# the image
RUN python src/manage.py collectstatic --noinput \
&& python src/manage.py compilemessages

EXPOSE 8000
CMD ["/start.sh"]
17 changes: 17 additions & 0 deletions bin/celery_beat.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

set -e

LOGLEVEL=${CELERY_LOGLEVEL:-INFO}

mkdir -p celerybeat

# Adding the database scheduler will also convert the CELERY_BEAT_SCHEDULE to
# database entries.

echo "Starting celery beat"
exec celery --workdir src --app "open_inwoner.celery" beat \
-l $LOGLEVEL \
-s ../celerybeat/beat \
--scheduler django_celery_beat.schedulers:DatabaseScheduler \
--pidfile= # empty on purpose, see https://github.com/open-formulieren/open-forms/issues/1182
14 changes: 14 additions & 0 deletions bin/celery_monitor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

set -e

LOGLEVEL=${CELERY_LOGLEVEL:-INFO}

# This monitors the tasks executed by Celery. The Celery worker needs to be
# started with the -E option to sent out the events.

echo "Starting celery events"
exec celery --workdir src --app "open_inwoner.celery" events \
-l $LOGLEVEL \
--camera django_celery_monitor.camera.Camera \
--frequency=2.0
joeribekker marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 8 additions & 2 deletions bin/celery_worker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,11 @@ if [[ "$ENABLE_COVERAGE" ]]; then
fi

echo "Starting celery worker $WORKER_NAME with queue $QUEUE"
exec $_binary --workdir src -A "open_inwoner" events -l info --camera django_celery_monitor.camera.Camera --frequency=2.0 &
exec $_binary --workdir src -A "open_inwoner" worker -l $LOGLEVEL -c $CONCURRENCY -Q $QUEUE -n $WORKER_NAME -E --max-tasks-per-child=50 -l info -B --scheduler django_celery_beat.schedulers:DatabaseScheduler
joeribekker marked this conversation as resolved.
Show resolved Hide resolved
exec $_binary --workdir src --app "open_inwoner.celery" worker \
-Q $QUEUE \
-n $WORKER_NAME \
-l $LOGLEVEL \
-O fair \
-c $CONCURRENCY \
-E \
--max-tasks-per-child=50
18 changes: 7 additions & 11 deletions bin/docker_start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@

set -ex

# Wait for the database container
# Figure out abspath of this script
SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")

# wait for required services
# See: https://docs.docker.com/compose/startup-order/
export PGHOST=${DB_HOST:-db}
export PGPORT=${DB_PORT:-5432}
${SCRIPTPATH}/wait_for_db.sh
joeribekker marked this conversation as resolved.
Show resolved Hide resolved

fixtures_dir=${FIXTURES_DIR:-/app/fixtures}
# fixtures_dir=${FIXTURES_DIR:-/app/fixtures}

uwsgi_port=${UWSGI_PORT:-8000}
uwsgi_processes=${UWSGI_PROCESSES:-4}
uwsgi_threads=${UWSGI_THREADS:-1}

until pg_isready; do
>&2 echo "Waiting for database connection..."
sleep 1
done

>&2 echo "Database is up."

# Apply database migrations
>&2 echo "Apply database migrations"
python src/manage.py migrate
Expand Down
7 changes: 6 additions & 1 deletion bin/setup_configuration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
# setup initial configuration using environment variables
# Run this script from the root of the repository

#set -e
set -e

# Figure out abspath of this script
SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")

${SCRIPTPATH}/wait_for_db.sh

src/manage.py migrate
Expand Down
9 changes: 0 additions & 9 deletions bin/start_celery.sh

This file was deleted.

2 changes: 1 addition & 1 deletion bin/wait_for_db.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ until pg_isready; do
sleep 1
done

>&2 echo "Database is up."
>&2 echo "Database is up."
69 changes: 65 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Inspired by https://docs.docker.com/compose/django/
version: '3'
#
# Docker-compose for full Open Inwoner stack.
#
# DISCLAIMER: THIS IS FOR DEVELOPMENT PURPOSES ONLY AND NOT SUITABLE FOR PRODUCTION.
#
# You can use this docker-compose to spin up a local Open Inwoner stack for demo/try-out
# purposes, or to get some insight in the various components involved (e.g. to build
# your Helm charts from). Note that various environment variables are UNSAFE and merely
# specified so that you can get up and running with the least amount of friction.
#
# Before deploying to production, please review the environment configuration reference:
# https://open-inwoner.readthedocs.io/
#
version: '3.4'

sergei-maertens marked this conversation as resolved.
Show resolved Hide resolved
services:
db:
Expand Down Expand Up @@ -44,6 +56,8 @@ services:
web:
build: &web_build
context: .
args:
RELEASE: ${TAG:-latest}
container_name: open-inwoner-web
image: maykinmedia/open-inwoner:${TAG:-latest}
environment: &web_env
Expand All @@ -60,9 +74,22 @@ services:
- CELERY_RESULT_BACKEND=redis://redis:6379/0
- CELERY_LOGLEVEL=DEBUG
- ES_HOST=elasticsearch
# Needed for Celery Flower to match the TIME_ZONE configured in the
# settings used by workers and beat containers.
- TZ=Europe/Amsterdam
# WARNING: Strictly for development!
- DISABLE_2FA=${DISABLE_2FA:-True}
- DEBUG=True
- IS_HTTPS=no
healthcheck:
test: ["CMD", "python", "-c", "import requests; exit(requests.head('http://localhost:8000/admin/').status_code not in [200, 302])"]
interval: 30s
timeout: 5s
retries: 3
# This should allow for enough time for migrations to run before the max
# retries have passed. This healthcheck in turn allows other containers
# to wait for the database migrations.
start_period: 60s
volumes: &web_volumes
- media:/app/media
- private_media:/app/private_media
Expand Down Expand Up @@ -91,10 +118,44 @@ services:
image: maykinmedia/open-inwoner:${TAG:-latest}
environment: *web_env
command: /celery_worker.sh
healthcheck:
test: ["CMD", "python", "/app/bin/check_celery_worker_liveness.py"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes: *web_volumes
depends_on:
- db
- redis
web:
# This health check condition is needed because Celery Beat will
# try to convert the CELERY_BEAT_SCHEDULE into database entries. For
# this, migrations need to be finished. If Celery tasks were still
# pending, the database also needs to be ready for Celery itself. We
# therefore have the health check here, and make Celery beat and
# monitor containers depend on the celery container.
condition: service_healthy
redis:
condition: service_started
networks:
- openinwoner-dev

celery-beat:
build: *web_build
image: maykinmedia/open-inwoner:${TAG:-latest}
environment: *web_env
command: /celery_beat.sh
depends_on:
- celery
networks:
- openinwoner-dev

celery-monitor:
build: *web_build
image: maykinmedia/open-inwoner:${TAG:-latest}
environment: *web_env
command: /celery_monitor.sh
depends_on:
- celery
networks:
- openinwoner-dev

Expand Down
48 changes: 46 additions & 2 deletions src/open_inwoner/celery.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from pathlib import Path

from django.conf import settings

from celery import Celery
from celery.signals import setup_logging
from celery import Celery, bootsteps
from celery.signals import setup_logging, worker_ready, worker_shutdown

from .setup import setup_env

Expand All @@ -26,3 +28,45 @@ def config_loggers(*args, **kwargs):
from logging.config import dictConfig

dictConfig(settings.LOGGING)


HEARTBEAT_FILE = Path(settings.BASE_DIR) / "tmp" / "celery_worker_heartbeat"
READINESS_FILE = Path(settings.BASE_DIR) / "tmp" / "celery_worker_ready"


#
# Utilities for checking the health of celery workers
#
class LivenessProbe(bootsteps.StartStopStep):
requires = {"celery.worker.components:Timer"}

def __init__(self, worker, **kwargs):
self.requests = []
self.tref = None

def start(self, worker):
self.tref = worker.timer.call_repeatedly(
10.0,
self.update_heartbeat_file,
(worker,),
priority=10,
)

def stop(self, worker):
HEARTBEAT_FILE.unlink(missing_ok=True)

def update_heartbeat_file(self, worker):
HEARTBEAT_FILE.touch()


@worker_ready.connect
def worker_ready(**_):
READINESS_FILE.touch()


@worker_shutdown.connect
def worker_shutdown(**_):
READINESS_FILE.unlink(missing_ok=True)


app.steps["worker"].add(LivenessProbe)
Binary file added src/open_inwoner/static/ico/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/open_inwoner/static/ico/favicon-96x96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/open_inwoner/static/ico/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading