diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..844960f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.devcontainer +.vscode +.claude +docs +log +**/__pycache__ +**/*.pyc diff --git a/.gitignore b/.gitignore index 0f55f8d..0d20b64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ *.pyc -log/ diff --git a/Dockerfile b/Dockerfile index c5be319..1e15bc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,39 @@ +# Build stage: compile the wheels (psycopg2 needs gcc and libpq-dev) +FROM python:3.15-slim-trixie AS build + +RUN apt-get update && \ + apt-get install --no-install-recommends -y gcc libc-dev libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /build +COPY requirements.txt . +RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt + +# Runtime stage: no compiler and no tools besides what db-setup.sh needs FROM python:3.15-slim-trixie -RUN apt -y update && apt -y upgrade && \ - apt install --no-install-recommends -y wait-for-it lsb-release wget gnupg2 gcc libpq-dev libc-dev vim postgresql-client && apt clean all +RUN apt-get update && apt-get upgrade -y && \ + apt-get install --no-install-recommends -y libpq5 postgresql-client wait-for-it && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=build /wheels /wheels +RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels + +RUN groupadd --system --gid 10001 max && \ + useradd --system --uid 10001 --gid max --no-create-home --shell /usr/sbin/nologin max WORKDIR /app -COPY requirements.txt . -RUN pip install -U -r requirements.txt - -RUN mkdir log - ENV PYTHONPATH=/app/ ENV FLASK_APP=max.app +# Owned by root, so the app cannot modify its own code COPY max/ ./max COPY scripts/prod/ . +# Precompile, since the app cannot write __pycache__ at runtime +RUN python -m compileall -q max + +USER max ENTRYPOINT ["./entrypoint.sh"] CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:8000", "max.app:app"] diff --git a/Makefile b/Makefile index d6fe6fc..9b53868 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,7 @@ run-dev: FLASK_APP=max.app FLASK_DEBUG=1 flask run --host 0.0.0.0 --port 8757 run-dev-as-prod: - mkdir -p log - FLASK_APP=max.app LOG_DIR=log MAX_ORIGIN=http://localhost:8757 gunicorn -w 1 -b 0.0.0.0:8757 max.app:app + FLASK_APP=max.app MAX_ORIGIN=http://localhost:8757 gunicorn -w 1 -b 0.0.0.0:8757 max.app:app run-cleanup: scripts/dev/cleanup.sh diff --git a/docker-compose.yml b/docker-compose.yml index 1469f99..94f2a10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,18 @@ services: - PGSQL_PASSWORD=max - PGSQL_NAME=max - MAX_ORIGIN=https://max.finn.st + read_only: true + tmpfs: + - /tmp # gunicorn worker heartbeat files + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" networks: - postgresql - uplink diff --git a/max/log.py b/max/log.py index 53dbb3e..ee1fc37 100644 --- a/max/log.py +++ b/max/log.py @@ -1,37 +1,23 @@ import logging -import os import sys from logging.config import dictConfig def init_logging(app): - handlers = { - "stdout": { - "class": "logging.StreamHandler", - "stream": sys.stdout, - "formatter": "default", - }, - } - active_handlers = ["stdout"] - if not app.debug: - handlers["file"] = { - "class": "logging.handlers.RotatingFileHandler", - "filename": os.path.join(os.environ.get("LOG_DIR", "/app/log"), "max.log"), - "formatter": "default", - "maxBytes": 1024 * 1024, - "backupCount": 5, - } - active_handlers.append("file") - dictConfig( { "version": 1, "formatters": { "default": {"format": "[%(asctime)s] [%(levelname)s]: %(message)s"} }, - "handlers": handlers, - "root": {"level": "INFO", "handlers": active_handlers}, - "gunicorn": {"level": "INFO", "handlers": active_handlers}, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "stream": sys.stdout, + "formatter": "default", + }, + }, + "root": {"level": "INFO", "handlers": ["stdout"]}, } ) logger = logging.getLogger(app.name)