From 5d3853f81b408a85bb7460b4e703d33a160ef769 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Mon, 16 Feb 2026 15:02:21 +0100 Subject: [PATCH] Production docker improvments. --- docker/Dockerfile | 2 +- docker/prod/.env.example | 20 ++++++++++++++++ docker/prod/Dockerfile | 42 ++++++++++++++++++++++++++-------- docker/prod/config/gunicorn.py | 17 ++++++++++++++ docker/prod/entrypoint.sh | 3 --- 5 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 docker/prod/.env.example create mode 100644 docker/prod/config/gunicorn.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 7eef4e1ecb..b029622231 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,7 +30,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ COPY ./ ./ # Create directories. -RUN mkdir -p ./hypha/static_compiled && mkdir -p ./hypha/media +RUN mkdir -p ./hypha/static_compiled && mkdir -p ./media # Build front end. RUN npm run dev:build diff --git a/docker/prod/.env.example b/docker/prod/.env.example new file mode 100644 index 0000000000..21f8791c20 --- /dev/null +++ b/docker/prod/.env.example @@ -0,0 +1,20 @@ +DJANGO_SETTINGS_MODULE="hypha.settings.production" +SECRET_KEY="changeme" + +PRIMARY_HOST="https://test.hypha.app" +EMAIL_HOST="hypha.app" + +EMAIL_SUBJECT_PREFIX="[Hypha]" +ORG_EMAIL="hello@hypha.app" +SERVER_EMAIL="test@hypha.app" + +DATABASE_URL="postgres://hypha:hypha@postgres:5432/hypha" +POSTGRES_USER=hypha +POSTGRES_PASSWORD=hypha +POSTGRES_DB=hypha +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 + +PYTHON_TIMEOUT=120 +PYTHON_THREADS=4 +PYTHON_WORKERS=2 diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 19a84abe4a..f164b04a89 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -15,8 +15,18 @@ ENV UV_LINK_MODE=copy # Set work directory. WORKDIR /opt/app +# Create the hypha user and group +ARG APP_UID=1000 +ARG APP_GID=1000 +RUN groupadd -g "${APP_GID}" hypha \ + && useradd --create-home --no-log-init -u "${APP_UID}" -g "${APP_GID}" hypha \ + && chown hypha:hypha -R /opt/app + +# Set user (default is otherwise root) +USER hypha + # Create directories. -RUN mkdir -p ./hypha/static_compiled && mkdir -p ./hypha/media +RUN mkdir -p ./hypha/static_compiled && mkdir -p ./media # Install node. COPY --from=node:24-slim /usr/local/bin /usr/local/bin @@ -26,7 +36,7 @@ COPY --from=node:24-slim /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin # Install node dependencies. -COPY package*.json ./ +COPY --chown=hypha:hypha package*.json ./ RUN npm install --quiet # Install python dependencies. @@ -36,7 +46,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev # Copy the project into the image -COPY ./ ./ +COPY --chown=hypha:hypha ./ ./ # Build front end. RUN npm run build && python manage.py collectstatic --noinput --verbosity=0 @@ -45,7 +55,7 @@ RUN npm run build && python manage.py collectstatic --noinput --verbosity=0 # # Production stage. # -FROM python:3.12-slim-bookworm +FROM python:3.12-slim-bookworm AS app # Add venv/bin to PATH. ENV PATH="/opt/app/.venv/bin:/usr/local/bin:$PATH" @@ -53,18 +63,30 @@ ENV PATH="/opt/app/.venv/bin:/usr/local/bin:$PATH" # Set work directory. WORKDIR /opt/app +# Create the hypha user and group +ARG APP_UID=1000 +ARG APP_GID=1000 +RUN groupadd -g "${APP_GID}" hypha \ + && useradd --create-home --no-log-init -u "${APP_UID}" -g "${APP_GID}" hypha \ + && chown hypha:hypha -R /opt/app + +# Set user (default is otherwise root) +USER hypha + # Create directories. -RUN mkdir -p ./hypha/static_compiled && mkdir -p ./hypha/media +RUN mkdir -p ./hypha/static_compiled && mkdir -p ./media # Copy venv and assets from builder. -COPY --from=builder /opt/app/.venv /opt/app/.venv -COPY --from=builder /opt/app/static /opt/app/static +COPY --chown=hypha:hypha --from=builder /opt/app/.venv /opt/app/.venv +COPY --chown=hypha:hypha --from=builder /opt/app/static /opt/app/static # Copy the project into the image -COPY ./ ./ +COPY --chown=hypha:hypha ./ ./ + +# Run entrypoint.sh. +ENTRYPOINT ["/opt/app/docker/prod/entrypoint.sh"] # Expose the port gunicorn is running on for other Docker containers. EXPOSE 8000 -# Run entrypoint.sh. -ENTRYPOINT ["/opt/app/docker/prod/entrypoint.sh"] +CMD ["gunicorn", "--config", "/opt/app/docker/prod/config/gunicorn.py"] diff --git a/docker/prod/config/gunicorn.py b/docker/prod/config/gunicorn.py new file mode 100644 index 0000000000..997ab14999 --- /dev/null +++ b/docker/prod/config/gunicorn.py @@ -0,0 +1,17 @@ +import multiprocessing +import os + +wsgi_app = "hypha.wsgi:application" +bind = "0.0.0.0:8000" + +errorlog = "-" # '-' logs to stderr +accesslog = "-" # '-' logs to stdout +access_log_format = ( + "%(h)s %(l)s %(u)s %(t)s '%(r)s' %(s)s %(b)s '%(f)s' '%(a)s' in %(D)sµs" # noqa: E501 +) + +worker_tmp_dir = "/dev/shm" +worker_class = "gthread" +workers = int(os.getenv("PYTHON_WORKERS", multiprocessing.cpu_count() * 2)) +threads = int(os.getenv("PYTHON_THREADS", 1)) +timeout = int(os.getenv("PYTHON_TIMEOUT", 120)) diff --git a/docker/prod/entrypoint.sh b/docker/prod/entrypoint.sh index 6569fa41d0..104953d94c 100755 --- a/docker/prod/entrypoint.sh +++ b/docker/prod/entrypoint.sh @@ -6,7 +6,4 @@ python manage.py migrate --noinput python manage.py clear_cache --cache=default python manage.py sync_roles -# Start gunicorn server. -gunicorn hypha.wsgi:application --env DJANGO_SETTINGS_MODULE=hypha.settings.production --worker-tmp-dir /dev/shm --workers=2 --threads=4 --worker-class=gthread --bind 0.0.0.0:8000 - exec "$@"