Add container image: two-stage uv build, non-root user, data volume
This commit is contained in:
1 parent
a39da9e615
commit
f266bc5104
3 files changed
+118
No files matched your search
@@ -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
|
||||
+61
@@ -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"]
|
||||
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user