From 6431ffeec9f80ec65b5ab2278224fd0f2bf88990 Mon Sep 17 00:00:00 2001 From: Joao Carreira Date: Tue, 6 Oct 2026 00:24:46 +0100 Subject: [PATCH] feat: separate docker compose files --- .env.example | 3 +++ AGENTS.md | 6 ++++-- Dockerfile | 10 ++++------ Makefile | 19 ++++++++++++++++--- README.md | 14 +++++++++++++- docker-compose.dev.yml | 32 ++++++++++++++++++++++++++++++++ docker-compose.yml | 15 +-------------- docs/ARCHITECTURE.md | 2 +- 8 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 docker-compose.dev.yml diff --git a/.env.example b/.env.example index efcb65f..ffa29c4 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,9 @@ SECRET_KEY=replace-with-a-long-random-value DATABASE_URL=sqlite:///./data/bar_rota.db BASE_URL=http://localhost:8000 +# Host directory bind-mounted at /app/data (used by docker compose only). +DATA_DIR=./data + # Initial admin is created only if no users exist. INITIAL_ADMIN_NAME=Administrator INITIAL_ADMIN_USERNAME=admin diff --git a/AGENTS.md b/AGENTS.md index d3e71e3..de22563 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,10 +63,12 @@ Prefer the containerized suite when the host Python environment is uncertain: ```bash make test-docker # or -docker compose --profile test run --rm --build tests +docker compose -f docker-compose.dev.yml --profile test run --rm tests ``` -The `test` target of the `Dockerfile` installs `requirements-dev.txt` on top of the app image and runs `pytest` with throwaway settings (`SECRET_KEY`, `DATABASE_URL`, `INITIAL_ADMIN_PASSWORD`). It mounts no host volumes and does not read `.env`, so it is safe on a fresh clone. +The `dev` target of the `Dockerfile` installs `requirements-dev.txt` on top of the app image; the `tests` service in `docker-compose.dev.yml` runs `pytest` with throwaway settings (`SECRET_KEY`, `DATABASE_URL`, `INITIAL_ADMIN_PASSWORD`). It mounts no host volumes and does not read `.env`, so it is safe on a fresh clone. + +`docker-compose.yml` is the service stack only. Anything development-related (auto-reload, bind mounts, test runner) belongs in `docker-compose.dev.yml`. For route/UI changes also start the app and verify `/health`, `/login`, the rota, admin month editor, and swap workflow. diff --git a/Dockerfile b/Dockerfile index 34bdb71..d974745 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,12 +12,10 @@ RUN mkdir -p /app/data EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] -# Test image: same app plus dev dependencies, runs the suite in isolation. -FROM base AS test +# Development image: base plus dev dependencies. Used by docker-compose.dev.yml, +# which supplies the reload command and per-service environment. +FROM base AS dev COPY requirements-dev.txt ./ RUN pip install --no-cache-dir -r requirements-dev.txt -ENV SECRET_KEY=test-only-secret-key \ - DATABASE_URL=sqlite:///./data/test.db \ - INITIAL_ADMIN_PASSWORD=test-only-password -CMD ["pytest", "-ra"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/Makefile b/Makefile index bf767d3..6d676eb 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,27 @@ -.PHONY: dev test test-docker init-db seed-demo reminders +COMPOSE ?= docker compose +COMPOSE_DEV = $(COMPOSE) -f docker-compose.dev.yml + +.PHONY: dev dev-up dev-down dev-logs test test-docker init-db seed-demo reminders dev: uvicorn app.main:app --reload +# Full app in Docker with bind mounts and auto-reload. +dev-up: + $(COMPOSE_DEV) up --build + +dev-down: + $(COMPOSE_DEV) down + +dev-logs: + $(COMPOSE_DEV) logs -f app + test: pytest -q # Runs the suite inside Docker, independent of the host Python environment. test-docker: - docker compose --profile test run --rm --build tests + $(COMPOSE_DEV) --profile test run --rm tests init-db: python -m app.cli init-db @@ -17,4 +30,4 @@ seed-demo: python -m app.cli seed-demo reminders: - python -m app.cli send-reminders + python -m app.cli send-reminders \ No newline at end of file diff --git a/README.md b/README.md index a7f8ce4..2f36647 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Small self-hosted rota application for a non-profit bar/club. ## Quick start with Docker +`docker-compose.yml` is the service stack (production/self-hosting): + ```bash cp .env.example .env # Edit SECRET_KEY and INITIAL_ADMIN_PASSWORD before first start. @@ -24,10 +26,20 @@ docker compose up -d --build Open `http://localhost:8000` and sign in with the values in `.env`. +`docker-compose.dev.yml` is the development stack (bind mounts, auto-reload, no restart policy): + +```bash +docker compose -f docker-compose.dev.yml up --build +# or +make dev-up +``` + **Important:** the initial admin is only created when the database has no users. Changing `INITIAL_ADMIN_PASSWORD` later does not reset that account; reset it from the People screen. ## Local development +Either use the dev container (above) or the host virtualenv: + ```bash python -m venv .venv . .venv/bin/activate @@ -72,7 +84,7 @@ Or in a container, so the host Python environment does not matter: ```bash make test-docker # or -docker compose --profile test run --rm --build tests +docker compose -f docker-compose.dev.yml --profile test run --rm tests ``` ## Project documentation diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..4fe6717 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,32 @@ +# Development stack: code is bind-mounted, reload is on, no restart policy. +# Run standalone: docker compose -f docker-compose.dev.yml up --build +# Tests: docker compose -f docker-compose.dev.yml run --rm tests +services: + app: + build: + context: . + target: dev + container_name: bar-rota-dev + env_file: + - path: .env + required: false + ports: + - "8000:8000" + volumes: + - ./app:/app/app + - ./tests:/app/tests + - ${DATA_DIR:-./data}:/app/data + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + + tests: + build: + context: . + target: dev + profiles: + - test + environment: + SECRET_KEY: test-only-secret-key + DATABASE_URL: sqlite:///./data/test.db + INITIAL_ADMIN_PASSWORD: test-only-password + PYTHONPATH: /app + command: ["pytest", "-ra"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d68118b..93f37ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,17 +9,4 @@ services: ports: - "8000:8000" volumes: - - ./data:/app/data - - tests: - build: - context: . - target: test - profiles: - - test - environment: - SECRET_KEY: test-only-secret-key - DATABASE_URL: sqlite:///./data/test.db - INITIAL_ADMIN_PASSWORD: test-only-password - PYTHONPATH: /app - command: ["pytest", "-ra"] + - ${DATA_DIR}:/app/data diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3b3b3a0..c33f655 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -23,6 +23,6 @@ Single FastAPI process with server-rendered HTML and a SQLite database volume. T ## Deployment -`docker-compose.yml` mounts `./data` at `/app/data`, so the SQLite DB persists across image rebuilds. +`docker-compose.yml` mounts `./data` at `/app/data`, so the SQLite DB persists across image rebuilds. It is the service stack only. `docker-compose.dev.yml` is the development stack: it builds the `dev` image target, bind-mounts `app/` and `tests/` for auto-reload, and hosts the containerized test runner. For larger installations, the SQLAlchemy configuration can point to PostgreSQL, but the project intentionally ships only the SQLite deployment path today.