# Dockerfile & uWSGI Reference

## `Dockerfile`

```dockerfile
FROM python:3.11-slim-bullseye AS base

RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    g++ \
    curl \
    libffi-dev \
    libssl-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /var/app

RUN python3 -m venv env
RUN env/bin/python3 -m pip install -q -U pip setuptools wheel
RUN env/bin/python3 -m pip install -q uwsgi

# Copy only what's needed for the application — never copy tests/ or .env into base
COPY application.py requirements.txt ./
COPY <svc>/ ./<svc>/
COPY spec/ ./spec/

RUN env/bin/python3 -m pip install --use-pep517 -I -r requirements.txt


FROM base AS deploy

ENV SNOWFLAKE_HOME=/var/app/.snowflake

# Non-root user — uWSGI runs as this user in production
RUN useradd uwsgi -s /bin/false
RUN chmod o+x /root
RUN chown uwsgi:uwsgi -R /var/app/

RUN mkdir -p /var/app/.snowflake && chmod 777 /var/app/.snowflake
RUN mkdir -p /var/log/uwsgi && chown -R uwsgi:uwsgi /var/log/uwsgi

COPY uwsgi-start.sh /
RUN chmod +x /uwsgi-start.sh

ENTRYPOINT ["/uwsgi-start.sh"]
EXPOSE 8080


FROM base AS pr_tests

# Separate stage for running tests in CI — includes test deps but not uwsgi entrypoint
COPY requirements-dev.txt ./
RUN env/bin/python3 -m pip install --use-pep517 -I -r requirements-dev.txt

COPY tests/ ./tests/
COPY setup.cfg mypy.ini .flake8 ./
```

**Dockerfile rules:**
- `base` stage: installs only production deps. Never copies `tests/`.
- `deploy` stage: creates `uwsgi` user with `-s /bin/false` (no shell). Sets `SNOWFLAKE_HOME`.
- `pr_tests` stage: extends `base` with dev deps and test files. Used in Jenkins `unit_lint_job`.
- `ADD . /var/app` is **never used** — it copies everything including `.env`. Use explicit `COPY` instead.
- Use `--use-pep517 -I` flags on all `pip install` calls.
- `SNOWFLAKE_HOME` must be set to a directory owned/writable by the `uwsgi` user — the Snowflake connector writes files there (4.4.0+).
- Jenkins builds with `dockerBuildTarget: 'deploy'` — only the `deploy` stage is pushed to ECR.

## `uwsgi-start.sh`

```bash
#!/bin/bash
set -e

UWSGI_BIN="env/bin/uwsgi"
DDTRACE_BIN="env/bin/ddtrace-run"

# Use ddtrace-run wrapper if available (adds APM to uWSGI master process)
# Note: --lazy-apps is required when using ddtrace with multiple workers to ensure
# each worker gets its own ddtrace tracer instance.
if [ -f "$DDTRACE_BIN" ]; then
    RUNNER="$DDTRACE_BIN $UWSGI_BIN"
else
    RUNNER="$UWSGI_BIN"
fi

exec $RUNNER \
    --module application:app \
    --master \
    --lazy-apps \
    --workers 250 \
    --cheaper 15 \
    --cheaper-algo spare \
    --cheaper-initial 15 \
    --cheaper-step 5 \
    --enable-threads \
    --http-socket 0.0.0.0:8080 \
    --harakiri 120 \
    --harakiri-verbose \
    --log-4xx \
    --log-5xx \
    --logformat '%(addr) - %(user) [%(ltime)] "%(method) %(uri) %(proto)" %(status) %(size) "%(referer)" "%(uagent)"' \
    --cache2 "name=uwsgi_cache,items=10" \
    --uid uwsgi \
    --gid uwsgi
```

**uWSGI rules:**
- `--module application:app` — the WSGI entry point.
- `--master --lazy-apps` — required with ddtrace; ensures each worker gets its own tracer.
- `--workers 250 --cheaper 15 --cheaper-algo spare --cheaper-initial 15 --cheaper-step 5` — starts with 15 workers, scales up to 250 under load, scales back down when idle.
- `--cheaper-initial 15` must equal `config.SNOWFLAKE_POOL_SIZE` to prevent connection exhaustion.
- `--enable-threads` — required for the Snowflake connector's internal threading.
- `--harakiri 120` — kills workers that don't respond within 120 seconds (prevents zombie workers).
- `--uid uwsgi --gid uwsgi` — drops to non-root user after binding the socket.

## `.flake8`

```ini
[flake8]
application-import-names = <svc>, tests
import-order-style = google
max-line-length = 88
ignore = E121,E123,E126,E226,E24,E704,W503,W504,D
exclude =
    .git,
    __pycache__,
    env/,
    .venv/
```

## `setup.cfg` (isort + pytest)

```ini
[isort]
profile = black
multi_line_output = 3
include_trailing_comma = True
line_length = 88
known_first_party = <svc>

[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
    --cov=<svc>
    --cov-report=xml:coverage.xml
    --cov-report=term-missing
    --junitxml=pyunit.xml
    -v
```

## `mypy.ini`

```ini
[mypy]
python_version = 3.11
strict = True
follow_imports = silent

[mypy-owsrequest.*]
ignore_missing_imports = True

[mypy-owslogger.*]
ignore_missing_imports = True

[mypy-oto.*]
ignore_missing_imports = True

[mypy-snowflake_connector.*]
ignore_missing_imports = True

[mypy-pythonfeatures.*]
ignore_missing_imports = True

[mypy-fakeredis.*]
ignore_missing_imports = True

[mypy-ddtrace.*]
ignore_missing_imports = True
```

## `requirements.txt`

```
-i https://pypi.theorchard.io/pypi

# Web
Flask==2.2.5
flask-apispec==0.11.4

# Internal Orchard libraries
owslogger==4.0.0
owsrequest==2.7.0
oto==3.1.0
snowflake_connector==2.1.0

# Data / DB
snowflake-connector-python==3.5.0
cryptography==41.0.7
SQLAlchemy==1.4.52
marshmallow==3.20.1

# Cache
redis==5.0.0
fakeredis==2.18.1

# Observability
ddtrace==2.21.2
sentry-sdk[flask]==1.40.5

# Utilities
python-dotenv==1.0.0
boto3==1.34.11
```

**Requirements rules:**
- Internal PyPI index `-i https://pypi.theorchard.io/pypi` must be the **first line**.
- All versions are **exactly pinned** — no `>=`, `~=`, or `^`.
- `fakeredis` is a production dependency (used as fallback when `REDIS_HOST` is unset).
- `boto3` is needed for `secrets_manager.flask_ext` (reads from AWS Secrets Manager in QA/prod).
- `python-dotenv` is needed to load `.env` in dev/test.

## `requirements-dev.txt`

```
-i https://pypi.theorchard.io/pypi

pytest==7.4.4
pytest-cov==4.1.0
pytest-mock==3.12.0
flake8==3.9.2
flake8-builtins==2.1.0
flake8-docstrings==1.7.0
flake8-import-order==0.18.2
flake8-quotes==3.3.2
mypy==1.10.1
black==24.4.2
isort==5.13.2
ipdb==0.13.13
```
