Make the app's port configurable via DEVPLACE_PORT

make dev/prod hardcoded uvicorn to port 10500 with no way to override it,
so config.PORT (and everything derived from it - INTERNAL_BASE_URL,
Devii's own base URL defaults) stayed 10500 regardless of what port the
process actually bound to. DEVPLACE_PORT now drives all of it, defaulting
to 10500. Docker's existing PORT var (the externally published port via
nginx) is unrelated and untouched; its app container pins DEVPLACE_PORT
to 10500 explicitly so a bare-metal .env value can never leak in and
desync it from the Dockerfile's fixed internal bind port.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 08:16:05 +00:00
co-authored by Claude Sonnet 5
parent 3d07a478c3
commit dcd90cc907
9 changed files with 25 additions and 8 deletions
+8 -1
View File
@@ -29,9 +29,16 @@ SECRET_KEY=change-me
# from the request. # from the request.
DEVPLACE_SITE_URL= 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=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 upload ceiling. Must be >= the admin-configurable max_upload_size_mb.
NGINX_MAX_BODY_SIZE=50m NGINX_MAX_BODY_SIZE=50m
+1
View File
@@ -116,6 +116,7 @@ Tests run on port 10501 with a tempfile SQLite DB and a dedicated `DEVPLACE_DATA
| Var | Default | Purpose | | Var | Default | Purpose |
|-----|---------|---------| |-----|---------|---------|
| `DEVPLACE_DATABASE_URL` | `sqlite:///<repo>/data/devplace.db` | Override DB path (tests use this) | | `DEVPLACE_DATABASE_URL` | `sqlite:///<repo>/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 | | `SECRET_KEY` | hardcoded fallback | Session signing |
| `DEVPLACE_DISABLE_SERVICES` | unset | When `1`, NewsService and other background services skip start (set by test conftest) | | `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 | | `PLAYWRIGHT_HEADLESS` | `1` in tests | Toggle headed mode |
+3 -2
View File
@@ -8,6 +8,7 @@ LOCUST_RUN_TIME ?= 120s
LOCUST_WEB_WORKERS ?= 4 LOCUST_WEB_WORKERS ?= 4
WEB_WORKERS ?= $(shell nproc 2>/dev/null || echo 2) WEB_WORKERS ?= $(shell nproc 2>/dev/null || echo 2)
DEVPLACE_RATE_LIMIT ?= 1000000 DEVPLACE_RATE_LIMIT ?= 1000000
DEVPLACE_PORT ?= 10500
VENV ?= $(CURDIR)/.venv VENV ?= $(CURDIR)/.venv
PYTHON := $(VENV)/bin/python PYTHON := $(VENV)/bin/python
@@ -38,10 +39,10 @@ install: $(PYTHON)
touch $(VENV_STAMP) touch $(VENV_STAMP)
dev: $(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) 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: delete-pyc:
find . -name "__pycache__" -type d -prune -exec rm -rf {} + 2>/dev/null || true find . -name "__pycache__" -type d -prune -exec rm -rf {} + 2>/dev/null || true
+3 -1
View File
@@ -260,6 +260,8 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable
|---------|---------|---------| |---------|---------|---------|
| `DEVPLACE_DATABASE_URL` | `sqlite:///<repo>/data/devplace.db` | Database connection string | | `DEVPLACE_DATABASE_URL` | `sqlite:///<repo>/data/devplace.db` | Database connection string |
| `DEVPLACE_DATA_DIR` | `<repo>/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_DATA_DIR` | `<repo>/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 | | `SECRET_KEY` | hardcoded fallback | Session signing key |
| `DEVPLACE_VAPID_SUB` | `mailto:retoor@molodetz.nl` | Contact address in the VAPID JWT `sub` claim | | `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 | | `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 ### 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=<other-port> make prod` (also honoured by `make dev`).
### Multi-worker safety ### Multi-worker safety
+1 -1
View File
@@ -45,7 +45,7 @@ SECRET_KEY = environ.get("SECRET_KEY", "devplace-secret-key-change-in-production
SECONDS_PER_DAY = 86400 SECONDS_PER_DAY = 86400
SESSION_MAX_AGE = SECONDS_PER_DAY * 7 SESSION_MAX_AGE = SECONDS_PER_DAY * 7
SESSION_MAX_AGE_REMEMBER = SECONDS_PER_DAY * 30 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("/") SITE_URL = environ.get("DEVPLACE_SITE_URL", "").rstrip("/")
PRESENCE_TIMEOUT_SECONDS = int(environ.get("DEVPLACE_PRESENCE_TIMEOUT_SECONDS", "60")) PRESENCE_TIMEOUT_SECONDS = int(environ.get("DEVPLACE_PRESENCE_TIMEOUT_SECONDS", "60"))
+2 -1
View File
@@ -11,12 +11,13 @@ from devplacepy.config import (
INTERNAL_MODEL, INTERNAL_MODEL,
DEVII_TASKS_DB, DEVII_TASKS_DB,
DEVII_LESSONS_DB, DEVII_LESSONS_DB,
PORT,
) )
DEFAULT_AI_URL = INTERNAL_GATEWAY_URL DEFAULT_AI_URL = INTERNAL_GATEWAY_URL
DEFAULT_AI_MODEL = INTERNAL_MODEL 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 CONTEXT_WINDOW_TOKENS = 1_048_576
MAX_OUTPUT_TOKENS = 384_000 MAX_OUTPUT_TOKENS = 384_000
SYSTEM_RESERVE_TOKENS = 64_000 SYSTEM_RESERVE_TOKENS = 64_000
+2 -1
View File
@@ -3,6 +3,7 @@
import logging import logging
import os import os
from devplacepy.config import PORT
from devplacepy.services.base import BaseService, ConfigField from devplacepy.services.base import BaseService, ConfigField
from . import config from . import config
@@ -18,7 +19,7 @@ from .tasks.scheduler import GlobalScheduler, retire
logger = logging.getLogger("devii.service") 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): class DeviiService(BaseService):
+1 -1
View File
@@ -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: 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. - 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. - `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). - `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).
+4
View File
@@ -13,6 +13,10 @@ services:
environment: environment:
HOME: /app HOME: /app
PYTHONDONTWRITEBYTECODE: "1" 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: volumes:
- .:/app - .:/app
expose: expose: