diff --git a/.env.example b/.env.example index 6d4402e..844494a 100644 --- a/.env.example +++ b/.env.example @@ -29,9 +29,16 @@ SECRET_KEY=change-me # from the request. DEVPLACE_SITE_URL= -# Host port the nginx front door binds. +# Host port the nginx front door binds (Docker only - the app container's own +# internal port stays fixed). PORT=10500 +# Port the uvicorn process itself binds to for `make dev`/`make prod` (bare +# metal, no Docker/nginx in front). Also what the app calls itself on +# internally (DEVII_BASE_URL default, INTERNAL_GATEWAY_URL). Unrelated to +# PORT above - leave unset unless running bare metal on a non-default port. +# DEVPLACE_PORT=10500 + # nginx upload ceiling. Must be >= the admin-configurable max_upload_size_mb. NGINX_MAX_BODY_SIZE=50m diff --git a/CLAUDE.md b/CLAUDE.md index 6ff54a7..60bac03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,7 @@ Tests run on port 10501 with a tempfile SQLite DB and a dedicated `DEVPLACE_DATA | Var | Default | Purpose | |-----|---------|---------| | `DEVPLACE_DATABASE_URL` | `sqlite:////data/devplace.db` | Override DB path (tests use this) | +| `DEVPLACE_PORT` | `10500` | Port the app itself binds to bare-metal (`make dev`/`make prod`, wired to uvicorn's `--port`; tests use their own `DEVPLACE_TEST_PORT`, default `10501`). Also `config.PORT`'s source, so `DEVPLACE_INTERNAL_BASE_URL`'s default and Devii's self-dial URL (`DEFAULT_BASE_URL`/`INSTANCE_ORIGIN_DEFAULT`) follow it automatically. The Docker app container pins it to `10500` in `docker-compose.yml` regardless of `.env` - Docker's externally reachable port is the unrelated `PORT` var (nginx's host mapping), never this one. | | `SECRET_KEY` | hardcoded fallback | Session signing | | `DEVPLACE_DISABLE_SERVICES` | unset | When `1`, NewsService and other background services skip start (set by test conftest) | | `PLAYWRIGHT_HEADLESS` | `1` in tests | Toggle headed mode | diff --git a/Makefile b/Makefile index 5c56c86..34bc75a 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ LOCUST_RUN_TIME ?= 120s LOCUST_WEB_WORKERS ?= 4 WEB_WORKERS ?= $(shell nproc 2>/dev/null || echo 2) DEVPLACE_RATE_LIMIT ?= 1000000 +DEVPLACE_PORT ?= 10500 VENV ?= $(CURDIR)/.venv PYTHON := $(VENV)/bin/python @@ -38,10 +39,10 @@ install: $(PYTHON) touch $(VENV_STAMP) dev: $(VENV_STAMP) - $(PYTHON) -m uvicorn devplacepy.main:app --reload --reload-dir devplacepy --host 0.0.0.0 --port 10500 --backlog 4096 + DEVPLACE_PORT=$(DEVPLACE_PORT) $(PYTHON) -m uvicorn devplacepy.main:app --reload --reload-dir devplacepy --host 0.0.0.0 --port $(DEVPLACE_PORT) --backlog 4096 prod: $(VENV_STAMP) - DEVPLACE_STATIC_VERSION=$$(date +%s) DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(WEB_WORKERS) $(PYTHON) -m uvicorn devplacepy.main:app --host 0.0.0.0 --port 10500 --workers $(WEB_WORKERS) --backlog 8192 --proxy-headers --forwarded-allow-ips '*' + DEVPLACE_STATIC_VERSION=$$(date +%s) DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(WEB_WORKERS) DEVPLACE_PORT=$(DEVPLACE_PORT) $(PYTHON) -m uvicorn devplacepy.main:app --host 0.0.0.0 --port $(DEVPLACE_PORT) --workers $(WEB_WORKERS) --backlog 8192 --proxy-headers --forwarded-allow-ips '*' delete-pyc: find . -name "__pycache__" -type d -prune -exec rm -rf {} + 2>/dev/null || true diff --git a/README.md b/README.md index 898c842..f65eb67 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,8 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable |---------|---------|---------| | `DEVPLACE_DATABASE_URL` | `sqlite:////data/devplace.db` | Database connection string | | `DEVPLACE_DATA_DIR` | `/data` | Single root for every runtime/user-generated artifact (DB, uploads, VAPID keys, locks, bot state, job staging, container workspaces), outside the package and not served via `/static`. Point at a volume in production. Defined once in `config.py` (`DATA_PATHS` registry, created by `ensure_data_dirs()`) | +| `DEVPLACE_PORT` | `10500` | Port the app itself binds to (`make dev`/`make prod`, and what `Makefile`'s `dev`/`prod` targets pass to uvicorn's `--port`). Also feeds `config.PORT`, so `DEVPLACE_INTERNAL_BASE_URL`'s default and Devii's own self-dial URL follow it automatically. Bare metal only - the Docker app container always binds its fixed internal port regardless of this var; for Docker, use `PORT` (below) to change the externally reachable port | +| `PORT` | `10500` | Docker only: the host port `docker-compose.yml` publishes nginx on (`127.0.0.1:${PORT}:80`). Unrelated to `DEVPLACE_PORT` above - it never reaches the app container | | `SECRET_KEY` | hardcoded fallback | Session signing key | | `DEVPLACE_VAPID_SUB` | `mailto:retoor@molodetz.nl` | Contact address in the VAPID JWT `sub` claim | | `DEVPLACE_INTERNAL_BASE_URL` | `http://localhost:10500` | Base URL the platform's own services dial for the AI gateway | @@ -1206,7 +1208,7 @@ The version sits in the **path**, not a query string, because the frontend is un ### Bare-metal alternative -`make prod` runs the same app without containers (`uvicorn ... --workers $(WEB_WORKERS) --proxy-headers`, where `WEB_WORKERS` defaults to `nproc`) from the project root, sharing the identical database and files. Note it binds port 10500, so it conflicts with the Docker front door on the same port - run one, or set a different `PORT`. +`make prod` runs the same app without containers (`uvicorn ... --workers $(WEB_WORKERS) --proxy-headers`, where `WEB_WORKERS` defaults to `nproc`) from the project root, sharing the identical database and files. It binds port 10500 by default, so it conflicts with the Docker front door on the same port - run one, or set `DEVPLACE_PORT= make prod` (also honoured by `make dev`). ### Multi-worker safety diff --git a/devplacepy/config.py b/devplacepy/config.py index dfd2719..5af0b22 100644 --- a/devplacepy/config.py +++ b/devplacepy/config.py @@ -45,7 +45,7 @@ SECRET_KEY = environ.get("SECRET_KEY", "devplace-secret-key-change-in-production SECONDS_PER_DAY = 86400 SESSION_MAX_AGE = SECONDS_PER_DAY * 7 SESSION_MAX_AGE_REMEMBER = SECONDS_PER_DAY * 30 -PORT = 10500 +PORT = int(environ.get("DEVPLACE_PORT", "10500")) SITE_URL = environ.get("DEVPLACE_SITE_URL", "").rstrip("/") PRESENCE_TIMEOUT_SECONDS = int(environ.get("DEVPLACE_PRESENCE_TIMEOUT_SECONDS", "60")) diff --git a/devplacepy/services/devii/config.py b/devplacepy/services/devii/config.py index b3e4fe7..939b2ff 100644 --- a/devplacepy/services/devii/config.py +++ b/devplacepy/services/devii/config.py @@ -11,12 +11,13 @@ from devplacepy.config import ( INTERNAL_MODEL, DEVII_TASKS_DB, DEVII_LESSONS_DB, + PORT, ) DEFAULT_AI_URL = INTERNAL_GATEWAY_URL DEFAULT_AI_MODEL = INTERNAL_MODEL -DEFAULT_BASE_URL = "http://127.0.0.1:10500" +DEFAULT_BASE_URL = f"http://127.0.0.1:{PORT}" CONTEXT_WINDOW_TOKENS = 1_048_576 MAX_OUTPUT_TOKENS = 384_000 SYSTEM_RESERVE_TOKENS = 64_000 diff --git a/devplacepy/services/devii/service.py b/devplacepy/services/devii/service.py index dd11c93..dfd24f6 100644 --- a/devplacepy/services/devii/service.py +++ b/devplacepy/services/devii/service.py @@ -3,6 +3,7 @@ import logging import os +from devplacepy.config import PORT from devplacepy.services.base import BaseService, ConfigField from . import config @@ -18,7 +19,7 @@ from .tasks.scheduler import GlobalScheduler, retire logger = logging.getLogger("devii.service") -INSTANCE_ORIGIN_DEFAULT = "http://127.0.0.1:10500" +INSTANCE_ORIGIN_DEFAULT = f"http://127.0.0.1:{PORT}" class DeviiService(BaseService): diff --git a/devplacepy/services/openai_gateway/CLAUDE.md b/devplacepy/services/openai_gateway/CLAUDE.md index 8d5c5a1..d53760a 100644 --- a/devplacepy/services/openai_gateway/CLAUDE.md +++ b/devplacepy/services/openai_gateway/CLAUDE.md @@ -118,7 +118,7 @@ The gateway records one row per upstream call (chat, vision, passthrough) and su The gateway is the only place that holds real provider URLs/models/keys. Every other AI consumer (news, bots, Devii guests) points at it by default and never touches a provider key: -- Defaults live in `config.py`: `INTERNAL_GATEWAY_URL` (`http://localhost:10500/openai/v1/chat/completions`, override with `DEVPLACE_INTERNAL_BASE_URL`) and `INTERNAL_MODEL` (`molodetz`). `news_ai_url`, `bot_api_url`, `devii_ai_url` default to `INTERNAL_GATEWAY_URL`; their model defaults to `molodetz`. +- Defaults live in `config.py`: `INTERNAL_GATEWAY_URL` (`http://localhost:{DEVPLACE_PORT, default 10500}/openai/v1/chat/completions`; the whole base can also be overridden with `DEVPLACE_INTERNAL_BASE_URL`) and `INTERNAL_MODEL` (`molodetz`). `news_ai_url`, `bot_api_url`, `devii_ai_url` default to `INTERNAL_GATEWAY_URL`; their model defaults to `molodetz`. - Each consumer's key falls back to `database.internal_gateway_key()` (reads the `gateway_internal_key` setting) when its own key field/env is unset - the provider-key fallbacks (`DEEPSEEK_API_KEY`/`OPENROUTER_API_KEY`) were removed from news and bots. - `gateway_force_model` (default on) and a `molodetz`/empty alias in `handle_chat` make the upstream always receive `gateway_model`, so `molodetz` is a stable generic alias. - `database.migrate_ai_gateway_settings()` (called at the end of `init_db()`, under the startup `init_lock`): generates `gateway_internal_key` (uuid4) if missing; migrates `DEEPSEEK_API_KEY`/`OPENROUTER_API_KEY` env into `gateway_api_key`/`gateway_vision_key` when the db value is empty; and rewrites any consumer AI URL still equal to the old `openai.app.molodetz.nl` default to the gateway, plus `bot_model` `deepseek-chat` -> `molodetz` (only uncustomized values). diff --git a/docker-compose.yml b/docker-compose.yml index 2379e80..4ea9a36 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,10 @@ services: environment: HOME: /app PYTHONDONTWRITEBYTECODE: "1" + # Pinned regardless of .env: the container always binds 10500 internally + # (Dockerfile CMD, nginx upstream). DEVPLACE_PORT is the bare-metal + # `make dev`/`make prod` knob; the externally reachable port is `PORT`. + DEVPLACE_PORT: "10500" volumes: - .:/app expose: