generated from john/python-template
V6.1 continue refining backup: switch to Synology Drive Client for remote backup
Quality Gate / gate (push) Failing after 1m10s
Quality Gate / gate (push) Failing after 1m10s
This commit is contained in:
@@ -3,7 +3,9 @@ set -eu
|
|||||||
|
|
||||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.production.yml}"
|
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.production.yml}"
|
||||||
ENV_FILE="${ENV_FILE:-.env.production}"
|
ENV_FILE="${ENV_FILE:-.env.production}"
|
||||||
BACKUP_DIR="${BACKUP_DIR:-./data/backups}"
|
BACKUP_DIR="${BACKUP_DIR:-${DATABASE_BACKUP_DIR:-./data/backups}}"
|
||||||
|
APP_DATA_BACKUP_DIR="${APP_DATA_BACKUP_DIR:-${BACKUP_DIR}/data}"
|
||||||
|
UPLOADS_BACKUP_DIR="${UPLOADS_BACKUP_DIR:-${BACKUP_DIR}/uploads}"
|
||||||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}"
|
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}"
|
||||||
SYNOLOGY_BACKUP_DIR="${SYNOLOGY_BACKUP_DIR:-}"
|
SYNOLOGY_BACKUP_DIR="${SYNOLOGY_BACKUP_DIR:-}"
|
||||||
|
|
||||||
@@ -66,6 +68,23 @@ find "${BACKUP_DIR}" -type f \( \
|
|||||||
-name 'backup-*.manifest' \
|
-name 'backup-*.manifest' \
|
||||||
\) -mtime +"${RETENTION_DAYS}" -delete
|
\) -mtime +"${RETENTION_DAYS}" -delete
|
||||||
|
|
||||||
|
# Always keep a rolling incremental mirror of uploaded files in BACKUP_DIR.
|
||||||
|
mkdir -p "${APP_DATA_BACKUP_DIR}"
|
||||||
|
if [ -d /app/data ]; then
|
||||||
|
cp -a /app/data/. "${APP_DATA_BACKUP_DIR}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${UPLOADS_BACKUP_DIR}/documents" "${UPLOADS_BACKUP_DIR}/photos"
|
||||||
|
if [ -f /app/uploads/homepage.md ] && [ ! -f "${UPLOADS_BACKUP_DIR}/homepage.md" ]; then
|
||||||
|
cp /app/uploads/homepage.md "${UPLOADS_BACKUP_DIR}/homepage.md"
|
||||||
|
fi
|
||||||
|
if [ -d /app/uploads/documents ]; then
|
||||||
|
cp -an /app/uploads/documents/. "${UPLOADS_BACKUP_DIR}/documents/"
|
||||||
|
fi
|
||||||
|
if [ -d /app/uploads/photos ]; then
|
||||||
|
cp -an /app/uploads/photos/. "${UPLOADS_BACKUP_DIR}/photos/"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
|
if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
|
||||||
mkdir -p "${SYNOLOGY_BACKUP_DIR}"
|
mkdir -p "${SYNOLOGY_BACKUP_DIR}"
|
||||||
|
|
||||||
@@ -89,21 +108,6 @@ if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
|
|||||||
--entrypoint sh app -lc \
|
--entrypoint sh app -lc \
|
||||||
"if [ -d /app/data/logs ]; then tar -C /app/data/logs -czf /backup/${logs_file} .; else : > /backup/${logs_file}; fi"
|
"if [ -d /app/data/logs ]; then tar -C /app/data/logs -czf /backup/${logs_file} .; else : > /backup/${logs_file}; fi"
|
||||||
|
|
||||||
# 3) Sync uploads incrementally to Synology (copy only new files).
|
|
||||||
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps \
|
|
||||||
-v "${SYNOLOGY_BACKUP_DIR}:/backup" \
|
|
||||||
--entrypoint sh app -lc '
|
|
||||||
mkdir -p /backup/uploads/documents /backup/uploads/photos;
|
|
||||||
if [ -f /app/uploads/homepage.md ] && [ ! -f /backup/uploads/homepage.md ]; then
|
|
||||||
cp /app/uploads/homepage.md /backup/uploads/homepage.md;
|
|
||||||
fi;
|
|
||||||
if [ -d /app/uploads/documents ]; then
|
|
||||||
cp -an /app/uploads/documents/. /backup/uploads/documents/;
|
|
||||||
fi;
|
|
||||||
if [ -d /app/uploads/photos ]; then
|
|
||||||
cp -an /app/uploads/photos/. /backup/uploads/photos/;
|
|
||||||
fi
|
|
||||||
'
|
|
||||||
else
|
else
|
||||||
if [ -d /app/data/logs ]; then
|
if [ -d /app/data/logs ]; then
|
||||||
tar -C /app/data/logs -czf "${SYNOLOGY_BACKUP_DIR}/${logs_file}" .
|
tar -C /app/data/logs -czf "${SYNOLOGY_BACKUP_DIR}/${logs_file}" .
|
||||||
@@ -111,16 +115,8 @@ if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
|
|||||||
: > "${SYNOLOGY_BACKUP_DIR}/${logs_file}"
|
: > "${SYNOLOGY_BACKUP_DIR}/${logs_file}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "${SYNOLOGY_BACKUP_DIR}/uploads/documents" "${SYNOLOGY_BACKUP_DIR}/uploads/photos"
|
mkdir -p "${SYNOLOGY_BACKUP_DIR}/uploads"
|
||||||
if [ -f /app/uploads/homepage.md ] && [ ! -f "${SYNOLOGY_BACKUP_DIR}/uploads/homepage.md" ]; then
|
cp -an "${UPLOADS_BACKUP_DIR}/." "${SYNOLOGY_BACKUP_DIR}/uploads/"
|
||||||
cp /app/uploads/homepage.md "${SYNOLOGY_BACKUP_DIR}/uploads/homepage.md"
|
|
||||||
fi
|
|
||||||
if [ -d /app/uploads/documents ]; then
|
|
||||||
cp -an /app/uploads/documents/. "${SYNOLOGY_BACKUP_DIR}/uploads/documents/"
|
|
||||||
fi
|
|
||||||
if [ -d /app/uploads/photos ]; then
|
|
||||||
cp -an /app/uploads/photos/. "${SYNOLOGY_BACKUP_DIR}/uploads/photos/"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cp "${BACKUP_DIR}/${manifest_file}" "${SYNOLOGY_BACKUP_DIR}/${manifest_file}"
|
cp "${BACKUP_DIR}/${manifest_file}" "${SYNOLOGY_BACKUP_DIR}/${manifest_file}"
|
||||||
@@ -137,12 +133,14 @@ fi
|
|||||||
echo "Created backup set:"
|
echo "Created backup set:"
|
||||||
echo " ${BACKUP_DIR}/${postgres_file}"
|
echo " ${BACKUP_DIR}/${postgres_file}"
|
||||||
echo " ${BACKUP_DIR}/${manifest_file}"
|
echo " ${BACKUP_DIR}/${manifest_file}"
|
||||||
|
echo " ${APP_DATA_BACKUP_DIR}/ (incremental app data mirror)"
|
||||||
|
echo " ${UPLOADS_BACKUP_DIR}/ (incremental uploads mirror)"
|
||||||
if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
|
if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
|
||||||
echo "Synology artifacts:"
|
echo "Synology artifacts:"
|
||||||
echo " ${SYNOLOGY_BACKUP_DIR}/${postgres_file}"
|
echo " ${SYNOLOGY_BACKUP_DIR}/${postgres_file}"
|
||||||
echo " ${SYNOLOGY_BACKUP_DIR}/${config_file}"
|
echo " ${SYNOLOGY_BACKUP_DIR}/${config_file}"
|
||||||
echo " ${SYNOLOGY_BACKUP_DIR}/${logs_file}"
|
echo " ${SYNOLOGY_BACKUP_DIR}/${logs_file}"
|
||||||
echo " ${SYNOLOGY_BACKUP_DIR}/uploads/{homepage.md,documents/,photos/} (incremental)"
|
echo " ${SYNOLOGY_BACKUP_DIR}/uploads/ (copied from local incremental mirror)"
|
||||||
echo " ${SYNOLOGY_BACKUP_DIR}/${manifest_file}"
|
echo " ${SYNOLOGY_BACKUP_DIR}/${manifest_file}"
|
||||||
else
|
else
|
||||||
echo "SYNOLOGY_BACKUP_DIR not set; skipped Synology copy."
|
echo "SYNOLOGY_BACKUP_DIR not set; skipped Synology copy."
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ services:
|
|||||||
- .env.production
|
- .env.production
|
||||||
environment:
|
environment:
|
||||||
RUN_EMBEDDED_WORKER: "false"
|
RUN_EMBEDDED_WORKER: "false"
|
||||||
|
RUNTIME_SETTINGS_ENV_FILE: "/app/.env.production"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -16,6 +17,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- app_uploads:/app/uploads
|
- app_uploads:/app/uploads
|
||||||
- app_data:/app/data
|
- app_data:/app/data
|
||||||
|
- ./backup:/backup
|
||||||
|
- ./.env.production:/app/.env.production
|
||||||
- ./prompts:/app/prompts:ro
|
- ./prompts:/app/prompts:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -36,6 +39,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- app_uploads:/app/uploads
|
- app_uploads:/app/uploads
|
||||||
- app_data:/app/data
|
- app_data:/app/data
|
||||||
|
- ./backup:/backup
|
||||||
|
- ./.env.production:/app/.env.production:ro
|
||||||
- ./prompts:/app/prompts:ro
|
- ./prompts:/app/prompts:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
disable: true
|
disable: true
|
||||||
|
|||||||
+18
-7
@@ -1,19 +1,21 @@
|
|||||||
# Backup and Restore (V6.0 Phase 4)
|
# Backup and Restore (V6.0 Phase 4)
|
||||||
|
|
||||||
This guide defines operational backup/restore for clean-slate recovery of the Docker runtime and Synology replication.
|
This guide defines operational backup/restore for clean-slate recovery of the Docker runtime with host-level backups that Synology Drive can replicate.
|
||||||
|
|
||||||
## 1. Backup artifacts (clean-slate recovery set)
|
## 1. Backup artifacts (clean-slate recovery set)
|
||||||
|
|
||||||
- Primary local backup location: `./data/backups`
|
- Primary local backup location: `DATABASE_BACKUP_DIR` (recommended production value: `/backup`)
|
||||||
- Local files created per run:
|
- Local files created per run:
|
||||||
- `postgres-YYYYMMDD-HHMMSS.dump` (PostgreSQL custom dump via `pg_dump -Fc`)
|
- `postgres-YYYYMMDD-HHMMSS.dump` (PostgreSQL custom dump via `pg_dump -Fc`)
|
||||||
- `backup-YYYYMMDD-HHMMSS.manifest` (artifact index)
|
- `backup-YYYYMMDD-HHMMSS.manifest` (artifact index)
|
||||||
- Synology files created per run (when `SYNOLOGY_BACKUP_DIR` is set):
|
- Optional Synology files created per run (when `SYNOLOGY_BACKUP_DIR` is set):
|
||||||
- `postgres-YYYYMMDD-HHMMSS.dump` (copied from local)
|
- `postgres-YYYYMMDD-HHMMSS.dump` (copied from local)
|
||||||
- `config-YYYYMMDD-HHMMSS.tar.gz` (deployment config snapshot: `.env.production`, `docker-compose.production.yml`, `deploy/cloudflared/config.yml` when present)
|
- `config-YYYYMMDD-HHMMSS.tar.gz` (deployment config snapshot: `.env.production`, `docker-compose.production.yml`, `deploy/cloudflared/config.yml` when present)
|
||||||
- `logs-YYYYMMDD-HHMMSS.tar.gz` (snapshot of `/app/data/logs`)
|
- `logs-YYYYMMDD-HHMMSS.tar.gz` (snapshot of `/app/data/logs`)
|
||||||
- `backup-YYYYMMDD-HHMMSS.manifest`
|
- `backup-YYYYMMDD-HHMMSS.manifest`
|
||||||
- Synology incremental media mirror (copied only if missing):
|
- Local app-data mirror (updated each run):
|
||||||
|
- `data/**`
|
||||||
|
- Local incremental media mirror (copied only if missing):
|
||||||
- `uploads/homepage.md`
|
- `uploads/homepage.md`
|
||||||
- `uploads/documents/**`
|
- `uploads/documents/**`
|
||||||
- `uploads/photos/**`
|
- `uploads/photos/**`
|
||||||
@@ -28,21 +30,30 @@ sh deploy/backup/create_postgres_backup.sh
|
|||||||
|
|
||||||
Environment overrides:
|
Environment overrides:
|
||||||
|
|
||||||
- `BACKUP_DIR` (default `./data/backups`)
|
- `BACKUP_DIR` (default `DATABASE_BACKUP_DIR`, then `./data/backups`)
|
||||||
|
- `APP_DATA_BACKUP_DIR` (default `${BACKUP_DIR}/data`)
|
||||||
|
- `UPLOADS_BACKUP_DIR` (default `${BACKUP_DIR}/uploads`)
|
||||||
- `BACKUP_RETENTION_DAYS` (default `14`)
|
- `BACKUP_RETENTION_DAYS` (default `14`)
|
||||||
- `SYNOLOGY_BACKUP_DIR` (if set, dump/config/log/manifest are copied to this mounted path and media is synced incrementally)
|
- `SYNOLOGY_BACKUP_DIR` (if set, dump/config/log/manifest are copied to this mounted path and media is synced incrementally)
|
||||||
- `ENV_FILE` (default `.env.production`)
|
- `ENV_FILE` (default `.env.production`)
|
||||||
- `COMPOSE_FILE` (default `docker-compose.production.yml`)
|
- `COMPOSE_FILE` (default `docker-compose.production.yml`)
|
||||||
|
|
||||||
|
Recommended production setup:
|
||||||
|
|
||||||
|
- Mount a host-visible folder into `/backup` for both `app` and `worker` services.
|
||||||
|
- Set `DATABASE_BACKUP_DIR=/backup` in `.env.production`.
|
||||||
|
- Leave `SYNOLOGY_BACKUP_DIR` empty when Synology Drive Client handles replication from the host folder.
|
||||||
|
|
||||||
Example with Synology mount:
|
Example with Synology mount:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SYNOLOGY_BACKUP_DIR=/mnt/synology-backups sh deploy/backup/create_postgres_backup.sh
|
SYNOLOGY_BACKUP_DIR=/mnt/synology-backups sh deploy/backup/create_postgres_backup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2.1 Persisting Synology mount (LXC)
|
## 2.1 Optional direct Synology mount (LXC)
|
||||||
|
|
||||||
The backup copy step depends on a mounted NAS path. Manual `mount` commands are lost after reboot unless persisted.
|
This section is only needed when using direct container-side Synology copy via `SYNOLOGY_BACKUP_DIR`.
|
||||||
|
Manual `mount` commands are lost after reboot unless persisted.
|
||||||
|
|
||||||
Use the template (copy, edit placeholders, then run):
|
Use the template (copy, edit placeholders, then run):
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ This runbook is the operational checklist for releasing and monitoring the trans
|
|||||||
|
|
||||||
1. Deploy artifact/config to target environment.
|
1. Deploy artifact/config to target environment.
|
||||||
- V6.0 Phase 1 production stack: `docker compose -f docker-compose.production.yml up -d --build`
|
- V6.0 Phase 1 production stack: `docker compose -f docker-compose.production.yml up -d --build`
|
||||||
|
- For Runtime Settings writes in production, mount `.env.production` into the app container and set `RUNTIME_SETTINGS_ENV_FILE=/app/.env.production`.
|
||||||
- For SQLite -> PostgreSQL cutover, run `uv run python tools/export_import_migration.py verify --source-db <sqlite-path-or-url> --target-db <postgres-url>` before switching runtime.
|
- For SQLite -> PostgreSQL cutover, run `uv run python tools/export_import_migration.py verify --source-db <sqlite-path-or-url> --target-db <postgres-url>` before switching runtime.
|
||||||
2. Validate service startup:
|
2. Validate service startup:
|
||||||
- `/healthz` responds `200`
|
- `/healthz` responds `200`
|
||||||
@@ -36,7 +37,7 @@ This runbook is the operational checklist for releasing and monitoring the trans
|
|||||||
- stdout aggregation receives events
|
- stdout aggregation receives events
|
||||||
- file logs are written under `./data/logs`
|
- file logs are written under `./data/logs`
|
||||||
5. Create a fresh PostgreSQL backup after successful deployment:
|
5. Create a fresh PostgreSQL backup after successful deployment:
|
||||||
- `sh deploy/backup/create_postgres_backup.sh` (creates local DB dump + Synology DB/config/log artifacts + incremental media mirror)
|
- `sh deploy/backup/create_postgres_backup.sh` (creates local DB dump + local incremental uploads mirror under `DATABASE_BACKUP_DIR`)
|
||||||
|
|
||||||
## 3. Rollback triggers and actions
|
## 3. Rollback triggers and actions
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ This runbook is the operational checklist for releasing and monitoring the trans
|
|||||||
1. Verify `postgres` container is healthy and accepting connections.
|
1. Verify `postgres` container is healthy and accepting connections.
|
||||||
2. Confirm dump file exists and is non-zero size.
|
2. Confirm dump file exists and is non-zero size.
|
||||||
3. Re-run backup/restore scripts with explicit `ENV_FILE` and `COMPOSE_FILE` if using non-default paths.
|
3. Re-run backup/restore scripts with explicit `ENV_FILE` and `COMPOSE_FILE` if using non-default paths.
|
||||||
4. If Synology copy fails, keep local backup and resolve mount/network before next backup cycle.
|
4. If direct Synology copy fails, keep local backup and resolve mount/network before next backup cycle.
|
||||||
- For LXC setups, use `deploy/backup/mount_synology_cifs.example.sh` as the persistent mount template.
|
- For LXC setups, use `deploy/backup/mount_synology_cifs.example.sh` as the persistent mount template.
|
||||||
|
|
||||||
## 6. Dependency upgrade policy
|
## 6. Dependency upgrade policy
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ Settings manages installation-local registries, safe runtime .env settings, and
|
|||||||
- **Runtime Settings**
|
- **Runtime Settings**
|
||||||
- Runtime Settings exposes an allowlisted set of non-secret fields synchronized with `Settings` model fields except excluded secret/unsafe fields.
|
- Runtime Settings exposes an allowlisted set of non-secret fields synchronized with `Settings` model fields except excluded secret/unsafe fields.
|
||||||
- Runtime Settings is rendered as a compact two-column editor (**Setting**, **Value**) in a centered, narrower responsive container.
|
- Runtime Settings is rendered as a compact two-column editor (**Setting**, **Value**) in a centered, narrower responsive container.
|
||||||
- Runtime Settings persists changes to `.env`, validates by constructing a `Settings` instance, and reports validation failures through the shared UI error presenter.
|
- Runtime Settings persists changes to the resolved runtime env file, validates by constructing a `Settings` instance, and reports validation failures through the shared UI error presenter.
|
||||||
|
- The write target resolution order is: explicit function override (tests/tools), `RUNTIME_SETTINGS_ENV_FILE` environment variable (deployment override), then `Settings.model_config.env_file` (default `.env`).
|
||||||
- Runtime Settings changes require application restart to take effect.
|
- Runtime Settings changes require application restart to take effect.
|
||||||
|
- Runtime Settings renders a host-side restart command (`docker compose -f docker-compose.production.yml up -d --force-recreate app worker`) so operators can apply saved values without granting Docker control to the app container.
|
||||||
- Runtime Settings includes an explicit "Other settings not shown here" markdown table listing:
|
- Runtime Settings includes an explicit "Other settings not shown here" markdown table listing:
|
||||||
- secrets (`OPENROUTER_API_KEY`, `DATABASE__PASSWORD`)
|
- secrets (`OPENROUTER_API_KEY`, `DATABASE__PASSWORD`)
|
||||||
- high-risk database connection settings (`DATABASE__DRIVER`, `DATABASE__PATH`, `DATABASE__HOST`, `DATABASE__PORT`, `DATABASE__DATABASE`, `DATABASE__USER`)
|
- high-risk database connection settings (`DATABASE__DRIVER`, `DATABASE__PATH`, `DATABASE__HOST`, `DATABASE__PORT`, `DATABASE__DATABASE`, `DATABASE__USER`)
|
||||||
|
|||||||
@@ -739,13 +739,23 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
|||||||
title="Runtime settings save failed",
|
title="Runtime settings save failed",
|
||||||
action=lambda: _write_runtime_settings(settings=settings, updates=updates),
|
action=lambda: _write_runtime_settings(settings=settings, updates=updates),
|
||||||
)
|
)
|
||||||
if not save_outcome.ok:
|
if not save_outcome.ok or save_outcome.value is None:
|
||||||
return
|
return
|
||||||
ui.notify("Runtime settings saved to .env", type="positive")
|
target_path = save_outcome.value.env_file_path
|
||||||
|
ui.notify(
|
||||||
|
f"Runtime settings saved to {target_path}. Restart app/worker to apply.",
|
||||||
|
type="positive",
|
||||||
|
)
|
||||||
render_runtime_settings.refresh()
|
render_runtime_settings.refresh()
|
||||||
|
|
||||||
with ui.row().classes("items-center gap-2 mt-3"):
|
with ui.row().classes("items-center gap-2 mt-3"):
|
||||||
ui.button("Save runtime settings", icon="save", on_click=save_runtime).classes("ui-btn-primary")
|
ui.button("Save runtime settings", icon="save", on_click=save_runtime).classes("ui-btn-primary")
|
||||||
|
with ui.column().classes("w-full max-w-3xl mx-auto gap-1 mt-2"):
|
||||||
|
ui.label("Apply saved runtime settings").classes("text-xs font-semibold")
|
||||||
|
ui.label(
|
||||||
|
"Run this on the host from the deployment repo to recreate app/worker with new env values:"
|
||||||
|
).classes("text-[11px] ui-text-muted")
|
||||||
|
ui.code(_runtime_restart_command(), language="bash").classes("w-full text-xs")
|
||||||
|
|
||||||
with ui.tabs().classes("w-full") as tabs:
|
with ui.tabs().classes("w-full") as tabs:
|
||||||
document_types_tab = ui.tab("Document Types")
|
document_types_tab = ui.tab("Document Types")
|
||||||
@@ -855,3 +865,7 @@ def _format_duration(*, started_at: datetime | None, finished_at: datetime | Non
|
|||||||
|
|
||||||
async def _read_maintenance_log(*, maintenance: MaintenanceService, log_path: str) -> bytes:
|
async def _read_maintenance_log(*, maintenance: MaintenanceService, log_path: str) -> bytes:
|
||||||
return await run_blocking(maintenance.read_log_bytes, log_path=log_path)
|
return await run_blocking(maintenance.read_log_bytes, log_path=log_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_restart_command() -> str:
|
||||||
|
return "docker compose -f docker-compose.production.yml up -d --force-recreate app worker"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -366,6 +367,10 @@ def _resolve_env_file_path(*, settings: Settings, env_file_path: Path | None) ->
|
|||||||
if env_file_path is not None:
|
if env_file_path is not None:
|
||||||
return env_file_path
|
return env_file_path
|
||||||
|
|
||||||
|
override = os.getenv("RUNTIME_SETTINGS_ENV_FILE", "").strip()
|
||||||
|
if override:
|
||||||
|
return Path(override)
|
||||||
|
|
||||||
configured = settings.model_config.get("env_file")
|
configured = settings.model_config.get("env_file")
|
||||||
if configured is None:
|
if configured is None:
|
||||||
return Path(".env")
|
return Path(".env")
|
||||||
|
|||||||
@@ -83,3 +83,18 @@ def test_save_runtime_settings_rejects_invalid_values(tmp_path: Path):
|
|||||||
updates={"port": "not-a-number"},
|
updates={"port": "not-a-number"},
|
||||||
env_file_path=env_path,
|
env_file_path=env_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_settings_uses_override_env_file(tmp_path: Path, monkeypatch):
|
||||||
|
settings = _settings_for_runtime_editing(tmp_path)
|
||||||
|
target = tmp_path / ".env.production"
|
||||||
|
target.write_text("OPENROUTER_API_KEY=test-key\nPORT=8000\n", encoding="utf-8")
|
||||||
|
monkeypatch.setenv("RUNTIME_SETTINGS_ENV_FILE", str(target))
|
||||||
|
|
||||||
|
snapshot = save_runtime_settings(
|
||||||
|
settings=settings,
|
||||||
|
updates={"port": "9001"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert snapshot.env_file_path == target
|
||||||
|
assert "PORT=9001" in target.read_text(encoding="utf-8")
|
||||||
|
|||||||
Reference in New Issue
Block a user