Files
cyberyan_linkedin_search/backend/Dockerfile
T
mobiniqow a464e550c9 feat: LinkedIn profile search engine (Django + React + Elasticsearch + ELK)
Full-stack implementation of the Cyberyan full-stack trial task.

Backend (Django 5 / DRF)
- Normalised PostgreSQL model (Profile, Company, School, Skill, Experience,
  Education) as the source of truth, with GIN + trigram indexes.
- Self-healing CSV reader: the supplied export carries an injected watermark
  column, missing columns, rows split by broken quoting and a duplicated
  header. A signature-scored Needleman-Wunsch alignment reconstructs 284 clean
  records out of 336 physical rows; only 10 rows parse correctly without it.
- Pluggable search layer (Strategy): Elasticsearch by default, PostgreSQL
  full-text search as a fallback selected by a single environment variable.
- Keyword search with per-field boosts, fuzziness and phrase boosting, six
  faceted filters (OR within a filter, AND across filters), an experience
  range, four sort orders, highlighting and facet aggregations.
- JWT auth with rotation + blacklist, three roles, PII masking, scoped rate
  limits, a single error envelope, OpenAPI schema and health probes.

Frontend (React 18 / TypeScript / Vite)
- Debounced search with type-ahead, faceted filter panel, result cards,
  profile drawer, URL-synced search state, automatic token refresh.
- Highlight snippets rendered without dangerouslySetInnerHTML.

Infrastructure
- Hardened multi-stage images (non-root, read-only rootfs, healthchecks).
- docker-compose for the full stack plus dev and production overlays.
- ELK: structured JSON logs shipped asynchronously to Logstash, routed per
  event type, with a composable index template and a Kibana setup script.

Quality
- 61 backend tests (85% coverage), ruff/black/bandit/mypy, ESLint + tsc,
  GitHub Actions running lint, tests and both image builds.
- Persian documentation: README plus architecture, search, API, data,
  security and operations guides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnFo6fNaHCTAkJbfcdkqtG
2026-09-03 08:13:50 +00:00

72 lines
2.8 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
COPY --chown=app:app backend/ /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.
RUN DJANGO_SECRET_KEY=build-only-not-a-real-secret-000000000000000000000000000000000000 \
DATABASE_URL=postgres://build:build@localhost:5432/build \
DJANGO_SETTINGS_MODULE=config.settings.dev \
python manage.py collectstatic --noinput --clear \
&& chmod +x /app/entrypoint.sh
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"]