From 5556bb2136a296c26a73422a121c4beef018ca6b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 21 Oct 2026 19:45:46 -0400 Subject: [PATCH] Compose files run on the orchestrator's defaults (#6220) Shipped stacks drop every healthcheck and the depends_on service_healthy wait, per rule 198 "Shipped compose files run on the orchestrator's defaults: no healthcheck, no restart policy". A pg_isready healthcheck turns slow storage into a killed database. The app needs neither: inkwell.dbwait waits up to 60s for Postgres at start, and the engine's pool_pre_ping retries per request. - docker-compose.yml: db and app healthchecks removed; depends_on is a plain start order. Comments cut to one or two lines per setting under rule 32 "Comments describe the product to a stranger": the family/rule references are gone, and the three-way port explanation points at docs/public-hosting.md, which already holds it. - docker-compose.dev.yml: db healthcheck and service_healthy removed; header shortened. - README: the copy-paste stack loses the same two blocks. restart: unless-stopped stays: plain Compose defaults to no restart, and Swarm ignores the key. Co-Authored-By: Claude Opus 5.5 --- README.md | 8 +--- docker-compose.dev.yml | 20 ++------- docker-compose.yml | 96 ++++++------------------------------------ 3 files changed, 17 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index a0d9c52..62a4d46 100644 --- a/README.md +++ b/README.md @@ -69,18 +69,12 @@ services: POSTGRES_DB: inkwell volumes: - inkwell-db:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U inkwell"] - interval: 5s - timeout: 5s - retries: 10 app: image: git.fabledsword.com/bvandeusen/inkwell:latest # :dev for the current dev build restart: unless-stopped depends_on: - db: - condition: service_healthy + - db environment: INKWELL_DATABASE_URL: postgresql+asyncpg://inkwell:CHANGE_ME@db:5432/inkwell volumes: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 23b2759..8ea03ce 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,15 +1,9 @@ -# Hot-reload DEV stack: Postgres + backend (hypercorn --reload) + Vite dev server. -# Edit code on the host and changes reload live. Per family rule 12 the agent does -# NOT start this — run it yourself: +# Development stack: Postgres, the backend with live reload, and the Vite dev server. # # docker compose -f docker-compose.dev.yml up # -# Then open http://localhost:5173 (the Vite dev server proxies /api to the backend). -# -# docker-compose.yml — the DEFAULT file — is the production stack instead: it pulls -# the published image, keeps Postgres off the host network, and expects a .env. This -# one builds nothing and is deliberately insecure-by-convenience (weak password, -# Postgres published on 5432) because it is meant for a laptop, not a deployment. +# Then open http://localhost:5173. For laptops only: the password is fixed and +# Postgres is published on 5432. docker-compose.yml is the deployment stack. services: db: image: postgres:16-alpine @@ -21,18 +15,12 @@ services: - inkwell-dev-db:/var/lib/postgresql/data ports: - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U inkwell"] - interval: 5s - timeout: 5s - retries: 10 api: image: python:3.12-slim working_dir: /app depends_on: - db: - condition: service_healthy + - db environment: INKWELL_DATABASE_URL: postgresql+asyncpg://inkwell:inkwell@db:5432/inkwell PYTHONPATH: /app/src diff --git a/docker-compose.yml b/docker-compose.yml index 9fa22dc..7f3ebfd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,11 @@ -# Inkwell — PRODUCTION stack (app + Postgres). -# -# This is the default compose file: `docker compose up -d` runs a real deployment -# from the published image. Development lives in docker-compose.dev.yml (hot-reload, -# builds from source). +# Inkwell: the app and its Postgres database, from the published image. # # cp .env.example .env # then set POSTGRES_PASSWORD # docker compose up -d # -# Per family rule 12 the agent does NOT start this — run it yourself. -# -# Upgrades: docker compose pull && docker compose up -d -# Rollback: set INKWELL_TAG to a commit sha in .env, then the same two commands. -# Every push publishes an immutable : image for exactly this. -# -# Schema migrations run automatically at container start (see the Dockerfile CMD), -# so an upgrade is just a pull and a restart. Take a backup first anyway: Settings → -# Backups → Back up now writes one file holding the notes AND their attachments -# (docs/backups.md). A bare pg_dump does not: attachments are files in the -# `inkwell-data` volume, not rows. +# Upgrade: `docker compose pull && docker compose up -d`. Migrations run at start. +# Back up first from Settings → Backups; a bare pg_dump misses the attachments, +# which live in the inkwell-data volume. services: db: @@ -25,98 +13,38 @@ services: restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-inkwell} - # No default on purpose. A production compose that ships a known password is - # how self-hosted databases end up in search engines; compose fails fast here - # instead, with the message below. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env — see .env.example} POSTGRES_DB: ${POSTGRES_DB:-inkwell} volumes: - # Which volume this resolves to is set at the bottom of this file, and an - # existing deployment overrides it from .env rather than renaming anything. - inkwell-db:/var/lib/postgresql/data - # Deliberately NOT published to the host. The app reaches Postgres over the - # compose network; exposing 5432 only widens the attack surface. If you need - # psql, `docker compose exec db psql -U inkwell` gets you there without it. - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-inkwell} -d ${POSTGRES_DB:-inkwell}"] - interval: 10s - timeout: 5s - retries: 10 - start_period: 30s + # Not published to the host; `docker compose exec db psql -U inkwell` reaches it. logging: &logging driver: json-file options: - # Unbounded container logs are a slow-motion disk-full outage on a - # long-running self-hosted box. max-size: "10m" max-file: "3" app: - # :latest tracks `main`. Set INKWELL_TAG=dev in .env to follow the - # development line instead, or a commit sha to pin exactly. + # latest follows main, dev follows dev, a commit sha pins one build. image: git.fabledsword.com/bvandeusen/inkwell:${INKWELL_TAG:-latest} restart: unless-stopped depends_on: - db: - condition: service_healthy + - db environment: - # The only required application setting. Everything else a person might want - # to tune lives in the admin Settings UI, backed by the database (rule 25). INKWELL_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-inkwell}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-inkwell} volumes: - # Uploaded attachments. /var/inkwell is fixed in the app (Config.DATA_DIR), - # not configurable — mount it or lose every image on container recreation. + # Uploaded attachments; without this mount they are lost when the container is recreated. - inkwell-data:/var/inkwell - # Daily backups to a folder (Settings → Backups, docs/backups.md): mount the - # share they should land in. Not needed when they go to an S3 bucket. + # Optional: the folder scheduled backups are written to (Settings → Backups). # - /mnt/backups/inkwell:/backups - # WHERE THE APP IS REACHABLE FROM. Three shapes, and the right answer is - # different for each — the default serves the first. - # - # 1. LAN, no proxy (the default). Binds every interface so your phone and your - # desktop can reach the server. This is what makes a self-hosted install work - # out of the box, and it is why the default is NOT the locked-down value: a - # server only reachable from the machine it runs on is not hardened, it is - # broken. - # - # 2. Reverse proxy in Docker, on this network (Traefik discovering the container, - # an nginx container, etc). DELETE the `ports:` block below entirely. The proxy - # reaches the app over the compose network without any port being published, - # and publishing one is a second, unauthenticated way in that bypasses the - # proxy — including whatever the proxy is doing about TLS and auth. - # - # 3. Reverse proxy on the HOST (not in Docker). Set INKWELL_BIND=127.0.0.1 in - # .env, so the port exists but only the host itself can reach it. - # - # If you are exposing this to the internet, you want 2 or 3. Leaving it at 1 - # means the app is reachable directly on port 5000, past everything your proxy - # does. + # Behind a reverse proxy in Docker, delete `ports:`; behind one on the host, set + # INKWELL_BIND=127.0.0.1. See docs/public-hosting.md. ports: - "${INKWELL_BIND:-0.0.0.0}:${INKWELL_PORT:-5000}:5000" - healthcheck: - # python rather than curl: the runtime image is python:3.12-slim and carries no - # HTTP client binary. Hits the app's own /api/health. - test: - - CMD - - python - - -c - - | - import sys, urllib.request - sys.exit(0 if urllib.request.urlopen("http://127.0.0.1:5000/api/health", timeout=5).status == 200 else 1) - interval: 30s - timeout: 10s - retries: 5 - # Generous: the container waits for Postgres and runs migrations before it - # serves anything, and a first boot builds the whole schema. - start_period: 60s logging: *logging volumes: - # Named explicitly so the name is one a person can type, and overridable so a - # deployment that predates the rename to Inkwell keeps the volumes it already has - # (see "Upgrading from ThoughtSync" in .env.example). Pointing these at the wrong - # name does not fail: it starts against an EMPTY database and the old one sits - # there untouched, which looks exactly like data loss. + # Overridable so an install from before the rename keeps its volumes (.env.example). inkwell-db: name: ${INKWELL_DB_VOLUME:-inkwell-db} inkwell-data: