Files
cyberyan_linkedin_search/backend/Dockerfile
T
mobiniqow 212074c5f5
CI / Backend (lint, types, tests) (push) Has been cancelled
CI / Frontend (lint, types, build) (push) Has been cancelled
CI / Docker images build (push) Has been cancelled
start project
2026-09-04 12:52:21 +03:30

111 lines
4.7 KiB
Docker

# syntax=docker/dockerfile:1.7
# =============================================================================
# Django API image.
#
# Two stages: wheels are built with a compiler present, the runtime image gets
# only the compiled artefacts. The result is small, has no build toolchain to
# exploit, and runs as an unprivileged user on a read-only-friendly layout.
# =============================================================================
# -----------------------------------------------------------------------------
# Stage 1 - build wheels
# -----------------------------------------------------------------------------
FROM python:3.12-slim-bookworm AS builder
ENV PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN apt-get update \
&& apt-get install --no-install-recommends -y build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# The build context is the repository root so the image can also carry the
# dataset; paths below are therefore prefixed with the directory name.
WORKDIR /build
COPY backend/requirements/ requirements/
RUN pip wheel --wheel-dir /wheels -r requirements/base.txt
# -----------------------------------------------------------------------------
# Stage 2 - runtime
# -----------------------------------------------------------------------------
FROM python:3.12-slim-bookworm AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
DJANGO_SETTINGS_MODULE=config.settings.prod \
PATH="/home/app/.local/bin:${PATH}"
# libpq5 is the only runtime library psycopg needs; curl backs the healthcheck.
RUN apt-get update \
&& apt-get install --no-install-recommends -y libpq5 curl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid 10001 app \
&& useradd --uid 10001 --gid app --create-home --shell /usr/sbin/nologin app
COPY --from=builder /wheels /wheels
COPY backend/requirements/ /tmp/requirements/
RUN pip install --no-index --find-links=/wheels -r /tmp/requirements/base.txt \
&& rm -rf /wheels /tmp/requirements
WORKDIR /app
# Explicit list, not `COPY backend/ /app/`: the runtime image must not ship
# test code (or stray repo files like this Dockerfile itself), only what the
# server needs to run. `tests/` is added back for the `dev` stage below.
COPY --chown=app:app backend/apps/ /app/apps/
COPY --chown=app:app backend/config/ /app/config/
COPY --chown=app:app backend/manage.py backend/entrypoint.sh backend/gunicorn.conf.py backend/pyproject.toml /app/
COPY --chown=app:app data/ /data/
# Static assets are only needed for the admin and the API docs, but baking them
# in keeps the container stateless. The step runs against the production
# settings on purpose: they are the ones this image ships with, and the dev
# module pulls in django-extensions which is not part of base.txt.
# WORKDIR creates /app as root before the COPY above populates it, and
# --chown on COPY only touches what it copies - the directory entry itself
# stays root-owned, which is enough to make anything that writes a *new* file
# straight into /app (e.g. pytest-cov's .coverage) fail with EACCES as the
# app user.
# The secret is generated per build and thrown away with the layer: nothing
# secret-shaped is ever committed to the repo or baked into the image, and
# collectstatic only needs *a* valid key, never a specific one. (A literal
# placeholder here would also trip the guard in config/settings/prod.py.)
RUN chown app:app /app \
&& chmod +x /app/entrypoint.sh \
&& DJANGO_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(64))')" \
DATABASE_URL=postgres://build:build@localhost:5432/build \
DJANGO_SETTINGS_MODULE=config.settings.prod \
python manage.py collectstatic --noinput --clear
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
CMD curl -fsS http://localhost:8000/api/v1/health/live/ || exit 1
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--config", "/app/gunicorn.conf.py"]
# -----------------------------------------------------------------------------
# Stage 3 - development
#
# Same layout as the runtime image plus the tooling from dev.txt (pytest,
# django-extensions, ...) so `config.settings.dev` can actually be imported.
# Selected through `target: dev` in docker-compose.dev.yml.
# -----------------------------------------------------------------------------
FROM runtime AS dev
USER root
ENV DJANGO_SETTINGS_MODULE=config.settings.dev
COPY backend/requirements/ /tmp/requirements/
RUN pip install -r /tmp/requirements/dev.txt \
&& rm -rf /tmp/requirements
# The one directory the runtime stage deliberately left out.
COPY --chown=app:app backend/tests/ /app/tests/
USER app