π§ Context
The Discord bot currently runs only as a local process (make discord β uv run python -m src.apps.discord_bot). To host it always-on later, it needs to run in a container. This ticket containerizes the bot and adds it to docker-compose.yml behind a profile, so it's available for deployment without changing the day-to-day workflow for contributors who only need Postgres/Redis for tests.
There is already a commented-out worker service in docker-compose.yml β it shows the exact pattern to follow (build: ., the internal database URL, depends_on). Reference it.
This is infrastructure + docs only β no application code changes. src/apps/discord_bot.py is not touched.
π Implementation Plan
-
Dockerfile (new, repo root). Build an image that runs the bot:
- Use uv's combined base image:
ghcr.io/astral-sh/uv:python3.12-bookworm-slim (also published as astral/uv:python3.12-bookworm-slim on Docker Hub). It bundles uv + Python 3.12 on Debian bookworm-slim, so there's no separate base image and no need to copy the uv binary in β this is the approach uv's own Docker guide recommends. The tag tracks the current uv for that Python/OS.
- Install dependencies from the committed lockfile with
uv sync --frozen so the image matches CI's resolution. Install dependencies before copying src/ so the dependency layer is cached across code changes.
- Copy
src/ and run the bot module as the container command.
- Follow uv's official container guide for the layer-caching and project-install details: https://docs.astral.sh/uv/guides/integration/docker/
-
.dockerignore (new, repo root). Keep the image small and avoid baking in secrets: exclude at least .venv, .git, tests, data, docker, __pycache__, and .env (the token must come in at runtime, never be built into the image).
-
docker-compose.yml β add a bot service behind a profile.
build: . (the new Dockerfile), command runs the bot module.
profiles: ["bot"] β this is the key requirement. With a profile set, a plain docker compose up does not start the bot; only docker compose --profile bot up does. This keeps the default test workflow (Postgres + Redis) unchanged.
depends_on: [postgres]. The bot does not use Redis at runtime, so don't depend on it.
env_file: .env to supply the token, guild ID, models, etc., plus two environment: overrides for the values that differ inside a container (see networking note below).
-
README.md β add a ## Bot Deployment section. Document: the prerequisites, the build/run commands, and the networking caveats. Keep all new content within this one new section to avoid conflicts with other contributors editing the README concurrently.
-
(Optional) add a make discord-docker target wrapping docker compose --profile bot up to mirror the existing discord target.
Container networking (host vs internal addresses)
A container does not share the host's localhost, so two settings from .env are wrong inside the container and must be overridden in the bot service's environment::
- Database β inside the compose network, Postgres is reached at
postgres:5432 (the service name + internal port), not localhost:5445. The commented worker service already shows this exact URL: postgresql+asyncpg://postgres:postgres@postgres:5432/cs_assistant.
- Ollama β Ollama runs on the host, not in compose, so the container reaches it via
host.docker.internal:11434 (OLLAMA_URL=http://host.docker.internal:11434). On Docker Desktop (macOS/Windows) this name resolves automatically; on Linux it does not, so add extra_hosts: ["host.docker.internal:host-gateway"] to the service (harmless on macOS/Windows, so include it unconditionally).
Everything else (DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, OLLAMA_CHAT_MODEL, EMBEDDING_DIM, TOP_K, β¦) comes from .env via env_file unchanged.
Prerequisites to document in the Deployment section
- Postgres running, with migrations and ingestion already run from the host (
make migrate + make ingest) β the bot container only reads the data; it does not migrate or ingest.
- Ollama running on the host with both models pulled.
.env populated with a valid DISCORD_BOT_TOKEN and DISCORD_GUILD_ID (the bot exits at startup without them).
π Notes
- Ollama stays on the host β it is intentionally not containerized (it holds the models and will use a GPU host in production). The container reaches out to it.
- No app code changes. If you find yourself editing
src/apps/discord_bot.py, stop β that's out of scope for this ticket.
- There are no automated tests for this ticket; it's verified by building and running the container (see below). The existing
make test still needs Docker for the DB tests.
- Keep README edits inside the new
## Deployment section.
β
Acceptance Criteria
- A
Dockerfile at the repo root builds an image that runs the bot, installing deps via uv sync --frozen.
- A
.dockerignore excludes .venv, .git, tests, data, and .env (at minimum).
docker-compose.yml has a bot service behind profiles: ["bot"]. A plain docker compose up -d does not start it (verify with docker compose ps β only postgres and redis come up); docker compose --profile bot up does.
- The
bot service reaches Postgres at postgres:5432 and host Ollama at host.docker.internal:11434, with extra_hosts set for Linux.
README.md has a ## Deployment section covering prerequisites (migrate + ingest from host, Ollama running, token in .env), the build/run commands, and the database/Ollama networking caveats including the Linux host-gateway note.
docker compose --profile bot up builds and starts the bot, it logs discord_ready, and /ask works in the dev server (manual check β needs a valid token).
make lint passes (the pre-commit check-yaml hook validates docker-compose.yml; trailing-whitespace / end-of-file hooks apply to the Dockerfile and README).
π§ Context
The Discord bot currently runs only as a local process (
make discordβuv run python -m src.apps.discord_bot). To host it always-on later, it needs to run in a container. This ticket containerizes the bot and adds it todocker-compose.ymlbehind a profile, so it's available for deployment without changing the day-to-day workflow for contributors who only need Postgres/Redis for tests.There is already a commented-out
workerservice indocker-compose.ymlβ it shows the exact pattern to follow (build: ., the internal database URL,depends_on). Reference it.This is infrastructure + docs only β no application code changes.
src/apps/discord_bot.pyis not touched.π Implementation Plan
Dockerfile(new, repo root). Build an image that runs the bot:ghcr.io/astral-sh/uv:python3.12-bookworm-slim(also published asastral/uv:python3.12-bookworm-slimon Docker Hub). It bundles uv + Python 3.12 on Debian bookworm-slim, so there's no separate base image and no need to copy theuvbinary in β this is the approach uv's own Docker guide recommends. The tag tracks the current uv for that Python/OS.uv sync --frozenso the image matches CI's resolution. Install dependencies before copyingsrc/so the dependency layer is cached across code changes.src/and run the bot module as the container command..dockerignore(new, repo root). Keep the image small and avoid baking in secrets: exclude at least.venv,.git,tests,data,docker,__pycache__, and.env(the token must come in at runtime, never be built into the image).docker-compose.ymlβ add abotservice behind a profile.build: .(the new Dockerfile), command runs the bot module.profiles: ["bot"]β this is the key requirement. With a profile set, a plaindocker compose updoes not start the bot; onlydocker compose --profile bot updoes. This keeps the default test workflow (Postgres + Redis) unchanged.depends_on: [postgres]. The bot does not use Redis at runtime, so don't depend on it.env_file: .envto supply the token, guild ID, models, etc., plus twoenvironment:overrides for the values that differ inside a container (see networking note below).README.mdβ add a## Bot Deploymentsection. Document: the prerequisites, the build/run commands, and the networking caveats. Keep all new content within this one new section to avoid conflicts with other contributors editing the README concurrently.(Optional) add a
make discord-dockertarget wrappingdocker compose --profile bot upto mirror the existingdiscordtarget.Container networking (host vs internal addresses)
A container does not share the host's
localhost, so two settings from.envare wrong inside the container and must be overridden in thebotservice'senvironment::postgres:5432(the service name + internal port), notlocalhost:5445. The commentedworkerservice already shows this exact URL:postgresql+asyncpg://postgres:postgres@postgres:5432/cs_assistant.host.docker.internal:11434(OLLAMA_URL=http://host.docker.internal:11434). On Docker Desktop (macOS/Windows) this name resolves automatically; on Linux it does not, so addextra_hosts: ["host.docker.internal:host-gateway"]to the service (harmless on macOS/Windows, so include it unconditionally).Everything else (
DISCORD_BOT_TOKEN,DISCORD_GUILD_ID,OLLAMA_CHAT_MODEL,EMBEDDING_DIM,TOP_K, β¦) comes from.envviaenv_fileunchanged.Prerequisites to document in the Deployment section
make migrate+make ingest) β the bot container only reads the data; it does not migrate or ingest..envpopulated with a validDISCORD_BOT_TOKENandDISCORD_GUILD_ID(the bot exits at startup without them).π Notes
src/apps/discord_bot.py, stop β that's out of scope for this ticket.make teststill needs Docker for the DB tests.## Deploymentsection.β Acceptance Criteria
Dockerfileat the repo root builds an image that runs the bot, installing deps viauv sync --frozen..dockerignoreexcludes.venv,.git,tests,data, and.env(at minimum).docker-compose.ymlhas abotservice behindprofiles: ["bot"]. A plaindocker compose up -ddoes not start it (verify withdocker compose psβ onlypostgresandrediscome up);docker compose --profile bot updoes.botservice reaches Postgres atpostgres:5432and host Ollama athost.docker.internal:11434, withextra_hostsset for Linux.README.mdhas a## Deploymentsection covering prerequisites (migrate + ingest from host, Ollama running, token in.env), the build/run commands, and the database/Ollama networking caveats including the Linuxhost-gatewaynote.docker compose --profile bot upbuilds and starts the bot, it logsdiscord_ready, and/askworks in the dev server (manual check β needs a valid token).make lintpasses (the pre-commitcheck-yamlhook validatesdocker-compose.yml; trailing-whitespace / end-of-file hooks apply to theDockerfileandREADME).