diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..99a4800
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,35 @@
+# Python
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.Python
+
+# Virtual environments
+.venv/
+venv/
+
+# Development tools
+.idea/
+.vscode/
+*.egg-info/
+
+# uv cache
+.uv/
+
+# Local output
+dist/
+
+# Don't bake data into the image — mount it at runtime
+# data.csv is excluded here so it must be provided via docker-compose volume
+data.csv
+
+# Git
+.git/
+.gitignore
+
+# Docker itself
+Dockerfile
+docker-compose.yml
+.dockerignore
+
diff --git a/.gitignore b/.gitignore
index 29caf69..1c69b55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.idea
+.venv
__pycache__/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..84a4b81
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,40 @@
+# syntax=docker/dockerfile:1
+FROM python:3.13-slim
+
+# Pull uv binary from the official image (no pip overhead)
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
+
+# Non-root user for security
+RUN adduser --disabled-password --no-create-home appuser
+
+WORKDIR /app
+
+# Install dependencies first (layer is cached unless lock file changes)
+COPY pyproject.toml uv.lock ./
+RUN uv sync --frozen --no-dev --no-install-project
+
+# Copy application source
+COPY src/ ./src/
+
+# data.csv is NOT baked into the image — it is mounted at runtime via docker-compose
+# so updates to the CSV don't require a rebuild.
+
+RUN chown -R appuser:appuser /app
+USER appuser
+
+# WORKDIR=/app → gunicorn CWD is /app → data.csv resolves to /app/data.csv (mounted volume)
+# PYTHONPATH=/app/src → 'app' module resolves to src/app.py
+ENV PYTHONPATH=/app/src
+
+EXPOSE 8000
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000')"
+
+CMD ["uv", "run", "--no-dev", "gunicorn", \
+ "--workers", "2", \
+ "--bind", "0.0.0.0:8000", \
+ "--access-logfile", "-", \
+ "--error-logfile", "-", \
+ "app:app"]
+
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..d72648a
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,506 @@
+
+
+
+
+
+
+ The Hope Pub Quiz
+
+
+
+
+
+
+
+
+
+
Performance Dashboard
+
The Hope Pub Quiz
+
Trying to convince ourselves we're not stupid
+
+
+
+
+
+
+
+ At a Glance
+
+
+
+ 23
+ Quizzes Played
+ Since Mar 2025
+
+
+
+ 61st
+ Avg. Relative Percentile
+ Lower = better finishing position
+
+
+
+ 4th place
+ Best Finish
+ Out of 12 teams
+
+
+
+ 68th
+ Recent Form
+ ↑ declining vs overall (lower is better)
+
+
+
+ 5
+ Top-Half Finishes
+ 22% of all games
+
+
+
+ 4 wks
+ Best Streak
+ Consecutive weeks attended
+
+
+
+
+
+
+
+
+
+
+
More Stats
+
+
+
+
- Top-half finishes
+ - 5 of 23 (22%)
+
+
+
+
- Average teams per night
+ - 11.8
+
+
+
+
- Average squad size
+ - 2.9 players
+
+
+
+
- Average Scattergories score
+ - 6.9 pts
+
+
+
+
- Best Scattergories score
+ - 11 pts
+
+
+
+
- Longest individual streak
+ - 14 weeks (Jay)
+
+
+
+
+
+
+
+
Squad
+
+
+
+
+ |
+ Player
+ |
+
+
+ Appearances
+ |
+
+
+ Avg. Relative Percentile
+ |
+
+
+ Spent
+ |
+
+
+
+
+
+
+
+ |
+ Jay
+ |
+
+
+ 22
+ |
+
+
+ 62nd
+ |
+
+
+ £66.00
+ |
+
+
+
+
+
+ |
+ Ciaran
+ |
+
+
+ 20
+ |
+
+
+ 62nd
+ |
+
+
+ £60.00
+ |
+
+
+
+
+
+ |
+ Sam
+ |
+
+
+ 9
+ |
+
+
+ 59th
+ |
+
+
+ £27.00
+ |
+
+
+
+
+
+ |
+ Ellora
+ |
+
+
+ 5
+ |
+
+
+ 52nd
+ |
+
+
+ £15.00
+ |
+
+
+
+
+
+ |
+ Drew
+ |
+
+
+ 3
+ |
+
+
+ 56th
+ |
+
+
+ £9.00
+ |
+
+
+
+
+
+ |
+ Tom
+ |
+
+
+ 2
+ |
+
+
+ 62nd
+ |
+
+
+ £6.00
+ |
+
+
+
+
+
+ |
+ Chloe
+ |
+
+
+ 2
+ |
+
+
+ 75th
+ |
+
+
+ £6.00
+ |
+
+
+
+
+
+ |
+ Theo
+ |
+
+
+ 1
+ |
+
+
+ 50th
+ |
+
+
+ £3.00
+ |
+
+
+
+
+
+ |
+ Jamie
+ |
+
+
+ 1
+ |
+
+
+ 50th
+ |
+
+
+ £3.00
+ |
+
+
+
+
+
+ |
+ Christine
+ |
+
+
+ 1
+ |
+
+
+ 50th
+ |
+
+
+ £3.00
+ |
+
+
+
+
+
+ |
+ Mide
+ |
+
+
+ 1
+ |
+
+
+ 100th
+ |
+
+
+ £3.00
+ |
+
+
+
+
+
+
+
+ |
+ Total
+ |
+
+
+ 67
+ |
+
+
+ —
+ |
+
+
+ £201.00
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 59ca144..1e0719b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"flask>=3.1.1",
+ "gunicorn>=23.0.0",
"pandas>=2.3.0",
"plotly>=6.1.2",
"scikit-learn>=1.7.0",
diff --git a/src/templates/index.html b/src/templates/index.html
index 26707bf..8c16ab5 100644
--- a/src/templates/index.html
+++ b/src/templates/index.html
@@ -15,7 +15,7 @@
Performance Dashboard
-
🍺 The Hope Pub Quiz
+
The Hope Pub Quiz
Trying to convince ourselves we're not stupid
diff --git a/uv.lock b/uv.lock
index 104a16d..b0eb93e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -49,6 +49,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
+[[package]]
+name = "gunicorn"
+version = "25.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" },
+]
+
[[package]]
name = "itsdangerous"
version = "2.2.0"
@@ -272,6 +284,7 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "flask" },
+ { name = "gunicorn" },
{ name = "pandas" },
{ name = "plotly" },
{ name = "scikit-learn" },
@@ -281,6 +294,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "flask", specifier = ">=3.1.1" },
+ { name = "gunicorn", specifier = ">=23.0.0" },
{ name = "pandas", specifier = ">=2.3.0" },
{ name = "plotly", specifier = ">=6.1.2" },
{ name = "scikit-learn", specifier = ">=1.7.0" },