diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5b0aa27 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# Secrets and the local-only launcher: must never reach a layer or the cache. +.secrets +.oauth_creds +start.sh +AGENTS.md + +# VCS and local assistant/tooling scratch +.git +.codegraph +.omp + +# Local virtualenv and build/runtime artifacts +.venv +dist +build +__pycache__ +*.py[oc] + +# Runtime state: internal service URLs and per-service header tokens stay on +# disk and get bind-mounted or volume-mounted into /app/data instead. +data diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7db3b63 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# syntax=docker/dockerfile:1 + +# UwuDash MK3 - one process serves the UI, the JSON API and the reverse proxy. +# +# docker build -t uwudash-mk3 . +# docker run -d -p 8000:8000 -v uwudash-data:/app/data uwudash-mk3 +# +# Two stages: the wheel is built from the src layout, then installed into a venv +# in a clean image, so the runtime layer carries no build tooling and no second +# copy of the source. data/ is a volume: the services store (and users.json) +# live there, so edits survive a restart and the host's internal URLs never get +# baked into a layer. + +ARG PYTHON_VERSION=3.11 + +# ---------------------------------------------------------------- build wheel +FROM python:${PYTHON_VERSION}-slim AS build +WORKDIR /app + +# README.md is part of this layer because pyproject.toml declares +# `readme = "README.md"`, which uv_build embeds into the wheel metadata. +COPY pyproject.toml uv.lock README.md /app/ +# Pinned: pyproject.toml requires the uv_build backend in >=0.13.5,<0.14.0, so an +# unpinned upgrade to uv 0.14 would break `uv build` against this manifest. +RUN pip install --no-cache-dir "uv==0.13.5" + +COPY src/uwudash_mk3 /app/src/uwudash_mk3 +RUN uv build --out-dir /app/dist + +# ------------------------------------------------------------------- runtime +FROM python:${PYTHON_VERSION}-slim AS runtime +WORKDIR /app + +ENV PYTHONUNBUFFERED=1 \ + UWUDASH_HOST=0.0.0.0 \ + UWUDASH_PORT=8000 \ + UWUDASH_DATA_DIR=/app/data + +COPY --from=build /app/dist/*.whl /tmp/wheels/ +RUN pip install --no-cache-dir "uv==0.13.5" \ + && uv venv /app/.venv \ + && uv pip install --python /app/.venv/bin/python --no-cache /tmp/wheels/*.whl \ + && rm -rf /tmp/wheels \ + && useradd -m -u 10001 appuser \ + && mkdir -p /app/data \ + && chown -R appuser:appuser /app \ + && chmod -R g=u /app + +USER appuser + +EXPOSE 8000 + +VOLUME /app/data + +# /health is the only route that needs no session, so it works with OIDC on. +# The port is read from the environment because UWUDASH_PORT is the knob users +# actually turn; the hardcoded default matches the ENV above. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD ["python", "-c", "import os,sys,urllib.request as r;sys.exit(0 if r.urlopen(f'http://127.0.0.1:{os.environ.get(\"UWUDASH_PORT\", \"8000\")}/health').status==200 else 1)"] + +ENTRYPOINT ["/app/.venv/bin/uwudash-mk3"] diff --git a/README.md b/README.md index 43a1911..7708991 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,42 @@ Development reload: uv run uwudash-mk3 --reload ``` +### Container + +```bash +docker build -t uwudash-mk3 . +docker run -d -p 8000:8000 -v uwudash-data:/app/data uwudash-mk3 +``` + +The image is two-stage: `uv build` produces the wheel from the `src/` layout, a +clean stage installs it into `/app/.venv`, and the entrypoint is the +`uwudash-mk3` console script. It runs as uid `10001` and binds `0.0.0.0:8000` +(`UWUDASH_HOST` / `UWUDASH_PORT` are baked in as `ENV` defaults - override both +the `--env` and the `-p` mapping if you change the port). + +`/app/data` is a `VOLUME` holding `services.json` (and `users.json` once auth is +on): a named volume keeps your service list across restarts, a bind mount +(`-v ./data:/app/data`) seeds the container from the host file instead. `data/` +is in `.dockerignore`, so internal URLs and per-service `headers` tokens never +land in an image layer. With a bind mount the directory must be writable by uid +`10001` (`chown -R 10001 data`); a named volume inherits the image's ownership. + +Sign-in from a container - secrets come from the environment, never the build +context (`.dockerignore` excludes `.secrets`, `.oauth_creds` and `start.sh`): + +```bash +docker run -d -p 8000:8000 -v uwudash-data:/app/data \ + --env UWUDASH_OIDC_ISSUER=https://id.example.com \ + --env UWUDASH_OIDC_CLIENT_ID=uwudash \ + --env UWUDASH_OIDC_CLIENT_SECRET=... \ + --env UWUDASH_SESSION_SECRET="$(openssl rand -hex 32)" \ + --env UWUDASH_PUBLIC_URL=https://dash.example.com \ + uwudash-mk3 +``` + +`HEALTHCHECK` polls `/health`, the one route that needs no session, so the +container stays `healthy` with auth on. + ### Command line flags | Flag | Default | Meaning |