Harden immutable release deployment
This commit is contained in:
@@ -69,6 +69,11 @@
|
|||||||
- Kept map rectangle and full-area selection available for coverage inspection
|
- Kept map rectangle and full-area selection available for coverage inspection
|
||||||
when a selected national theme has not yet been materialized, without
|
when a selected national theme has not yet been materialized, without
|
||||||
querying an unrelated active dataset.
|
querying an unrelated active dataset.
|
||||||
|
- Hardened the all-in-one release path with commit-SHA image tags, OCI build
|
||||||
|
labels, previous-image preservation, automatic/manual rollback, an isolated
|
||||||
|
fresh-install smoke and dependency-cache-safe build metadata.
|
||||||
|
- Made production startup reject known-default PostGIS passwords and apply the
|
||||||
|
configured upload limit consistently to nginx and FastAPI.
|
||||||
|
|
||||||
## Sprint 240 Operational forest, agriculture, nature and soil themes (2026-07-17)
|
## Sprint 240 Operational forest, agriculture, nature and soil themes (2026-07-17)
|
||||||
|
|
||||||
|
|||||||
@@ -167,9 +167,11 @@ def test_frontend_uses_same_origin_api_proxy_by_default() -> None:
|
|||||||
def test_nginx_runtime_allows_real_gis_upload_payloads() -> None:
|
def test_nginx_runtime_allows_real_gis_upload_payloads() -> None:
|
||||||
frontend_nginx = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
|
frontend_nginx = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
|
||||||
all_in_one_nginx = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
|
all_in_one_nginx = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
|
||||||
|
start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "client_max_body_size 250m;" in frontend_nginx
|
assert "client_max_body_size 250m;" in frontend_nginx
|
||||||
assert "client_max_body_size 250m;" in all_in_one_nginx
|
assert "client_max_body_size __GEOINTEL_MAX_UPLOAD_MB__m;" in all_in_one_nginx
|
||||||
|
assert 'sed -i "s/__GEOINTEL_MAX_UPLOAD_MB__/${MAX_UPLOAD_MB}/g"' in start_script
|
||||||
|
|
||||||
|
|
||||||
def test_nginx_runtime_allows_long_ai_and_qa_requests() -> None:
|
def test_nginx_runtime_allows_long_ai_and_qa_requests() -> None:
|
||||||
@@ -314,11 +316,11 @@ def test_all_in_one_dockerfile_caches_dependencies_and_uses_cpu_torch_for_ai_run
|
|||||||
def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
|
def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
|
||||||
deploy_ps1 = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
deploy_ps1 = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||||
deploy_sh = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
deploy_sh = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||||
|
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||||
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
|
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert 'DEPLOY_GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-}"' in deploy_sh
|
assert 'DEPLOY_GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-}"' in deploy_sh
|
||||||
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in deploy_sh
|
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
|
||||||
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in deploy_ps1
|
|
||||||
assert "DEPLOY_GEOINTEL_INSTALL_AI" in deploy_ps1
|
assert "DEPLOY_GEOINTEL_INSTALL_AI" in deploy_ps1
|
||||||
assert "[string]$InstallAi" in deploy_ps1
|
assert "[string]$InstallAi" in deploy_ps1
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_identity_does_not_invalidate_dependency_layers() -> None:
|
||||||
|
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
dependency_install = dockerfile.index("/usr/bin/python3.11 -m venv /opt/geointel/venv")
|
||||||
|
source_copy = dockerfile.index("COPY backend/ /app/")
|
||||||
|
build_identity = dockerfile.index("ARG GEOINTEL_BUILD_SHA=unknown")
|
||||||
|
|
||||||
|
assert build_identity > dependency_install
|
||||||
|
assert build_identity > source_copy
|
||||||
|
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
|
||||||
|
assert 'org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}"' in dockerfile
|
||||||
|
|
||||||
|
|
||||||
|
def test_release_deploy_preserves_immutable_and_previous_images() -> None:
|
||||||
|
script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert 'GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}"' in script
|
||||||
|
assert 'GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"' in script
|
||||||
|
assert 'docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}"' in script
|
||||||
|
assert 'docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"' in script
|
||||||
|
assert "rollback_previous()" in script
|
||||||
|
assert "Deployed immutable image" in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_configuration_is_validated_before_container_replacement() -> None:
|
||||||
|
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
validation_index = run_script.index("validate_runtime_config")
|
||||||
|
replacement_index = run_script.index("docker compose down")
|
||||||
|
|
||||||
|
assert validation_index < replacement_index
|
||||||
|
assert "known-default PostGIS password" in run_script
|
||||||
|
assert "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048" in run_script
|
||||||
|
assert 'docker image inspect "$GEOINTEL_IMAGE"' in run_script
|
||||||
|
|
||||||
|
|
||||||
|
def test_fresh_install_smoke_is_isolated_and_cleans_only_its_temp_path() -> None:
|
||||||
|
script = (ROOT / "scripts" / "verify_release_fresh_install.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "mktemp -d" in script
|
||||||
|
assert "geointel-fresh-smoke.*" in script
|
||||||
|
assert "-p 127.0.0.1::80" in script
|
||||||
|
assert "GEOINTEL_POSTGRES_PASSWORD=" in script
|
||||||
|
assert "/health/ready" in script
|
||||||
|
assert "/api/v1/system/capabilities" in script
|
||||||
|
assert "docker exec" in script
|
||||||
|
assert "python -m alembic heads" in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_rollback_reuses_persistent_paths_and_requires_existing_image() -> None:
|
||||||
|
rollback = (ROOT / "deploy" / "unraid" / "rollback-dockerman-container.sh").read_text(encoding="utf-8")
|
||||||
|
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "geointel-all-in-one:previous" in rollback
|
||||||
|
assert 'docker image inspect "$GEOINTEL_ROLLBACK_IMAGE"' in rollback
|
||||||
|
assert 'GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE"' in rollback
|
||||||
|
assert '-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data"' in run_script
|
||||||
|
assert '-v "${GEOINTEL_STORAGE_PATH}:/app/storage"' in run_script
|
||||||
|
|
||||||
|
|
||||||
|
def test_readiness_checks_all_release_shell_entrypoints() -> None:
|
||||||
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for path in (
|
||||||
|
"scripts/deploy_tower.sh",
|
||||||
|
"scripts/verify_release_fresh_install.sh",
|
||||||
|
"deploy/unraid/all-in-one-start.sh",
|
||||||
|
"deploy/unraid/run-dockerman-container.sh",
|
||||||
|
"deploy/unraid/deploy-release.sh",
|
||||||
|
"deploy/unraid/rollback-dockerman-container.sh",
|
||||||
|
):
|
||||||
|
assert f"bash -n {path}" in readiness
|
||||||
@@ -36,13 +36,16 @@ def test_runtime_report_is_read_only_by_default_and_requires_confirmation() -> N
|
|||||||
|
|
||||||
def test_all_in_one_deploy_embeds_immutable_build_identity() -> None:
|
def test_all_in_one_deploy_embeds_immutable_build_identity() -> None:
|
||||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||||
|
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||||
deploy_powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
deploy_powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||||
deploy_shell = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
deploy_shell = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "ARG GEOINTEL_BUILD_SHA=unknown" in dockerfile
|
assert "ARG GEOINTEL_BUILD_SHA=unknown" in dockerfile
|
||||||
assert 'GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}"' in dockerfile
|
assert 'GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}"' in dockerfile
|
||||||
assert 'GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"' in dockerfile
|
assert 'GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"' in dockerfile
|
||||||
|
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
|
||||||
|
assert 'GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"' in release_script
|
||||||
|
assert "--build-arg GEOINTEL_BUILD_SHA=" in release_script
|
||||||
|
assert "--build-arg GEOINTEL_BUILD_TIME=" in release_script
|
||||||
for deploy_source in (deploy_powershell, deploy_shell):
|
for deploy_source in (deploy_powershell, deploy_shell):
|
||||||
assert 'GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"' in deploy_source
|
assert "bash deploy/unraid/deploy-release.sh" in deploy_source
|
||||||
assert "--build-arg GEOINTEL_BUILD_SHA=" in deploy_source
|
|
||||||
assert "--build-arg GEOINTEL_BUILD_TIME=" in deploy_source
|
|
||||||
|
|||||||
@@ -524,8 +524,11 @@ def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
|||||||
def test_tower_deploy_waits_for_startup_migration_before_live_smoke() -> None:
|
def test_tower_deploy_waits_for_startup_migration_before_live_smoke() -> None:
|
||||||
for relative_path in ("scripts/deploy_tower.ps1", "scripts/deploy_tower.sh"):
|
for relative_path in ("scripts/deploy_tower.ps1", "scripts/deploy_tower.sh"):
|
||||||
script = (ROOT / relative_path).read_text(encoding="utf-8")
|
script = (ROOT / relative_path).read_text(encoding="utf-8")
|
||||||
wait_position = script.index("wait_for_geointel_health")
|
assert "bash deploy/unraid/deploy-release.sh" in script
|
||||||
invocation_position = script.index("\nwait_for_geointel_health", wait_position)
|
|
||||||
smoke_position = script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh")
|
release_script = (ROOT / "deploy/unraid/deploy-release.sh").read_text(encoding="utf-8")
|
||||||
assert "docker inspect --format" in script
|
wait_position = release_script.index("wait_for_geointel_health")
|
||||||
assert invocation_position < smoke_position
|
invocation_position = release_script.index("\n wait_for_geointel_health", wait_position)
|
||||||
|
smoke_position = release_script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh")
|
||||||
|
assert "docker inspect --format" in release_script
|
||||||
|
assert invocation_position < smoke_position
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def test_unraid_template_documents_editable_runtime_settings() -> None:
|
|||||||
template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8")
|
template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "<Name>geointel</Name>" in template
|
assert "<Name>geointel</Name>" in template
|
||||||
assert "GeoIntel all-in-one runs the complete GeoIntel Kempen V1 stack in one Docker container" in template
|
assert "Belgium and Belgian North Sea workbench" in template
|
||||||
assert "<Repository>geointel-all-in-one:latest</Repository>" in template
|
assert "<Repository>geointel-all-in-one:latest</Repository>" in template
|
||||||
assert "<WebUI>http://[IP]:[PORT:80]/</WebUI>" in template
|
assert "<WebUI>http://[IP]:[PORT:80]/</WebUI>" in template
|
||||||
assert "<Icon>http://192.168.10.150:1202/geointel-icon.png</Icon>" in template
|
assert "<Icon>http://192.168.10.150:1202/geointel-icon.png</Icon>" in template
|
||||||
@@ -109,29 +109,35 @@ def test_unraid_all_in_one_runtime_starts_embedded_postgis_backend_and_nginx() -
|
|||||||
def test_tower_deploy_uses_single_container_unraid_compose() -> None:
|
def test_tower_deploy_uses_single_container_unraid_compose() -> None:
|
||||||
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||||
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||||
|
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
for script in (powershell, bash):
|
for script in (powershell, bash):
|
||||||
assert "docker compose -f docker-compose.unraid.yml config" in script
|
assert "bash deploy/unraid/deploy-release.sh" in script
|
||||||
assert "--build-arg GEOINTEL_INSTALL_AI=" in script
|
|
||||||
assert '--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA"' in script
|
assert "docker compose -f docker-compose.unraid.yml config" in release_script
|
||||||
assert '--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME"' in script
|
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
|
||||||
assert "-f deploy/unraid/Dockerfile.all-in-one" in script
|
assert '--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA"' in release_script
|
||||||
assert "-t geointel-all-in-one:latest" in script
|
assert '--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME"' in release_script
|
||||||
assert "docker compose -f docker-compose.unraid.yml build geointel" not in script
|
assert "-f deploy/unraid/Dockerfile.all-in-one" in release_script
|
||||||
assert "bash deploy/unraid/run-dockerman-container.sh" in script
|
assert '-t "$GEOINTEL_RELEASE_IMAGE"' in release_script
|
||||||
assert "LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh" in script
|
assert '-t "${GEOINTEL_IMAGE_REPOSITORY}:latest"' in release_script
|
||||||
|
assert 'GEOINTEL_IMAGE="$image" bash deploy/unraid/run-dockerman-container.sh' in release_script
|
||||||
|
assert "LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh" in release_script
|
||||||
|
|
||||||
|
|
||||||
def test_tower_deploy_build_uses_remote_env_ai_setting_by_default() -> None:
|
def test_tower_deploy_build_uses_remote_env_ai_setting_by_default() -> None:
|
||||||
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
|
||||||
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
|
||||||
|
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
for script in (powershell, bash):
|
for script in (powershell, bash):
|
||||||
assert "if [ -f .env ]; then" in script
|
|
||||||
assert ". ./.env" in script
|
|
||||||
assert "DEPLOY_GEOINTEL_INSTALL_AI" in script
|
assert "DEPLOY_GEOINTEL_INSTALL_AI" in script
|
||||||
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"' in script
|
assert "bash deploy/unraid/deploy-release.sh" in script
|
||||||
assert 'docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI"' in script
|
|
||||||
|
assert "if [ -f .env ]; then" in release_script
|
||||||
|
assert ". ./.env" in release_script
|
||||||
|
assert 'GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"' in release_script
|
||||||
|
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
|
||||||
|
|
||||||
|
|
||||||
def test_powershell_tower_deploy_streams_remote_script_to_bash() -> None:
|
def test_powershell_tower_deploy_streams_remote_script_to_bash() -> None:
|
||||||
|
|||||||
@@ -12,13 +12,9 @@ ARG GEOINTEL_INSTALL_AI=false
|
|||||||
ARG GEOINTEL_TORCH_INDEX_URL=https://download.pytorch.org/whl/cpu
|
ARG GEOINTEL_TORCH_INDEX_URL=https://download.pytorch.org/whl/cpu
|
||||||
ARG GEOINTEL_TORCH_VERSION=2.13.0
|
ARG GEOINTEL_TORCH_VERSION=2.13.0
|
||||||
ARG GEOINTEL_TORCHVISION_VERSION=0.28.0
|
ARG GEOINTEL_TORCHVISION_VERSION=0.28.0
|
||||||
ARG GEOINTEL_BUILD_SHA=unknown
|
|
||||||
ARG GEOINTEL_BUILD_TIME=unknown
|
|
||||||
|
|
||||||
ENV GEOINTEL_ENV=production \
|
ENV GEOINTEL_ENV=production \
|
||||||
GEOINTEL_API_PREFIX=/api/v1 \
|
GEOINTEL_API_PREFIX=/api/v1 \
|
||||||
GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
|
|
||||||
GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}" \
|
|
||||||
GEOINTEL_STORAGE_ROOT=/app/storage \
|
GEOINTEL_STORAGE_ROOT=/app/storage \
|
||||||
STORAGE_ROOT=/app/storage \
|
STORAGE_ROOT=/app/storage \
|
||||||
GEOINTEL_ALL_IN_ONE=1 \
|
GEOINTEL_ALL_IN_ONE=1 \
|
||||||
@@ -154,6 +150,17 @@ RUN chmod +x /usr/local/bin/geointel-all-in-one-start \
|
|||||||
/app/scripts/run_background_corpus_split_matrix.sh \
|
/app/scripts/run_background_corpus_split_matrix.sh \
|
||||||
/app/scripts/run_split_background_promotion_workflow.sh
|
/app/scripts/run_split_background_promotion_workflow.sh
|
||||||
|
|
||||||
|
ARG GEOINTEL_BUILD_SHA=unknown
|
||||||
|
ARG GEOINTEL_BUILD_TIME=unknown
|
||||||
|
|
||||||
|
ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
|
||||||
|
GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="GeoIntel" \
|
||||||
|
org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \
|
||||||
|
org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}" \
|
||||||
|
org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}"
|
||||||
|
|
||||||
VOLUME ["/var/lib/postgresql/data", "/app/storage"]
|
VOLUME ["/var/lib/postgresql/data", "/app/storage"]
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
+60
-7
@@ -72,11 +72,19 @@ cd /mnt/user/appdata/geointel
|
|||||||
cp deploy/unraid/geointel.env.example .env
|
cp deploy/unraid/geointel.env.example .env
|
||||||
nano .env
|
nano .env
|
||||||
docker compose -f docker-compose.unraid.yml config
|
docker compose -f docker-compose.unraid.yml config
|
||||||
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
bash deploy/unraid/deploy-release.sh
|
||||||
bash deploy/unraid/run-dockerman-container.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The repository deploy scripts run the same flow automatically. They validate the Compose reference, build the image with the `GEOINTEL_INSTALL_AI` build arg, install the DockerMan template/icon, remove any old Compose-owned `geointel` container, preserve/migrate the PostGIS data path and start the final container with DockerMan labels.
|
Set `GEOINTEL_POSTGRES_PASSWORD` to a unique value before that first start.
|
||||||
|
Production startup fails before replacing the active container when the
|
||||||
|
password is empty or one of the documented defaults.
|
||||||
|
|
||||||
|
The repository deploy scripts run the same flow automatically. They validate
|
||||||
|
the Compose reference, preserve the current image as
|
||||||
|
`geointel-all-in-one:previous`, build an immutable commit-SHA tag plus `latest`,
|
||||||
|
install the DockerMan metadata and start the SHA-tagged image. A failed start,
|
||||||
|
live migration smoke or browser/API smoke automatically attempts the previous
|
||||||
|
image without changing the configured PostGIS or storage paths.
|
||||||
|
|
||||||
`scripts/deploy_tower.sh` and `scripts/deploy_tower.ps1` source the remote
|
`scripts/deploy_tower.sh` and `scripts/deploy_tower.ps1` source the remote
|
||||||
`.env` before building the image. That means `GEOINTEL_INSTALL_AI=true` in
|
`.env` before building the image. That means `GEOINTEL_INSTALL_AI=true` in
|
||||||
@@ -99,8 +107,9 @@ libraries needed for Ultralytics imports; it still never downloads model weights
|
|||||||
The documented CPU runtime installs pinned `torch==2.13.0` and
|
The documented CPU runtime installs pinned `torch==2.13.0` and
|
||||||
`torchvision==0.28.0` from `https://download.pytorch.org/whl/cpu`, avoiding the
|
`torchvision==0.28.0` from `https://download.pytorch.org/whl/cpu`, avoiding the
|
||||||
unused CUDA runtime wheels included by the general Linux package index. The
|
unused CUDA runtime wheels included by the general Linux package index. The
|
||||||
Dockerfile copies dependency metadata before backend source, so normal code-only
|
Dockerfile copies dependency metadata before backend source and applies
|
||||||
redeploys can reuse the expensive dependency layer.
|
commit/build metadata only after the heavy file layers. Normal code-only
|
||||||
|
redeploys therefore reuse the apt, GIS and optional PyTorch dependency layers.
|
||||||
`YOLO_CONFIG_DIR` defaults to `/app/storage/ultralytics`, a writable persistent
|
`YOLO_CONFIG_DIR` defaults to `/app/storage/ultralytics`, a writable persistent
|
||||||
path, so Ultralytics settings do not fall back to root user config directories.
|
path, so Ultralytics settings do not fall back to root user config directories.
|
||||||
|
|
||||||
@@ -173,8 +182,7 @@ GEOINTEL_CORS_ORIGINS=http://localhost:1203,http://127.0.0.1:1203,http://192.168
|
|||||||
Apply:
|
Apply:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
bash deploy/unraid/deploy-release.sh
|
||||||
bash deploy/unraid/run-dockerman-container.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Persistent paths
|
## Persistent paths
|
||||||
@@ -226,10 +234,55 @@ curl http://127.0.0.1:1202/api/v1/assistant/models
|
|||||||
cd /mnt/user/appdata/geointel
|
cd /mnt/user/appdata/geointel
|
||||||
git fetch origin main
|
git fetch origin main
|
||||||
git reset --hard origin/main
|
git reset --hard origin/main
|
||||||
|
bash deploy/unraid/deploy-release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The equivalent low-level build remains available for debugging:
|
||||||
|
|
||||||
|
```bash
|
||||||
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
|
||||||
bash deploy/unraid/run-dockerman-container.sh
|
bash deploy/unraid/run-dockerman-container.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Release identity, fresh install and rollback
|
||||||
|
|
||||||
|
Inspect the running immutable revision and retained images:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' geointel
|
||||||
|
docker image ls geointel-all-in-one
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a fresh install in isolated temporary PostGIS, storage and model paths. The
|
||||||
|
script binds only a random loopback port and removes its own container and
|
||||||
|
temporary directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/verify_release_fresh_install.sh geointel-all-in-one:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Return to the image that was active immediately before the latest deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/unraid/rollback-dockerman-container.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
For an older retained commit, select its immutable tag explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GEOINTEL_ROLLBACK_IMAGE=geointel-all-in-one:<commit-sha> \
|
||||||
|
bash deploy/unraid/rollback-dockerman-container.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Rollback reuses the configured PostGIS and storage mounts and never runs an
|
||||||
|
Alembic downgrade. If a future release has a backward-incompatible migration,
|
||||||
|
restore its verified pre-release backup instead of forcing an older app
|
||||||
|
against a newer schema.
|
||||||
|
|
||||||
|
The configured upload limit is shared by FastAPI and the generated nginx
|
||||||
|
runtime configuration. Values outside `1..2048` MiB are rejected before the
|
||||||
|
active application is replaced.
|
||||||
|
|
||||||
## Safe cleanup
|
## Safe cleanup
|
||||||
|
|
||||||
Safe cache cleanup if Docker build cache fills the Unraid Docker image:
|
Safe cache cleanup if Docker build cache fills the Unraid Docker image:
|
||||||
|
|||||||
@@ -13,6 +13,27 @@ export YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"
|
|||||||
export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"
|
export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"
|
||||||
export GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP="${GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP:-true}"
|
export GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP="${GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP:-true}"
|
||||||
|
|
||||||
|
case "$MAX_UPLOAD_MB" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "GEOINTEL_MAX_UPLOAD_MB must be a whole number between 1 and 2048." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "$MAX_UPLOAD_MB" -lt 1 ] || [ "$MAX_UPLOAD_MB" -gt 2048 ]; then
|
||||||
|
echo "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${GEOINTEL_ENV:-production}:${POSTGRES_PASSWORD}" in
|
||||||
|
production:|production:geointel|production:postgres|production:password|production:changeme|production:change-me-before-shared-use)
|
||||||
|
echo "Refusing to start production with an empty or known-default PostGIS password." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sed -i "s/__GEOINTEL_MAX_UPLOAD_MB__/${MAX_UPLOAD_MB}/g" /etc/nginx/conf.d/default.conf
|
||||||
|
nginx -t
|
||||||
|
|
||||||
mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR" /run/nginx /var/log/nginx
|
mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR" /run/nginx /var/log/nginx
|
||||||
chown -R postgres:postgres "$PGDATA"
|
chown -R postgres:postgres "$PGDATA"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [ -f .env ]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. ./.env
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
|
||||||
|
GEOINTEL_INSTALL_AI="$DEPLOY_GEOINTEL_INSTALL_AI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
|
||||||
|
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
|
||||||
|
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}"
|
||||||
|
GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}"
|
||||||
|
GEOINTEL_PREVIOUS_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:previous"
|
||||||
|
FRONTEND_URL="${FRONTEND_URL:-http://127.0.0.1:${GEOINTEL_FRONTEND_PORT:-1202}}"
|
||||||
|
|
||||||
|
wait_for_geointel_health() {
|
||||||
|
local status=""
|
||||||
|
for attempt in $(seq 1 90); do
|
||||||
|
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
||||||
|
if [ "$status" = "healthy" ]; then
|
||||||
|
echo "GeoIntel container is healthy after attempt ${attempt}."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
|
||||||
|
echo "GeoIntel container entered terminal state: ${status}" >&2
|
||||||
|
docker logs --tail 120 geointel >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2
|
||||||
|
docker logs --tail 120 geointel >&2 || true
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_image() {
|
||||||
|
local image="$1"
|
||||||
|
GEOINTEL_IMAGE="$image" bash deploy/unraid/run-dockerman-container.sh
|
||||||
|
wait_for_geointel_health
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback_previous() {
|
||||||
|
if ! docker image inspect "$GEOINTEL_PREVIOUS_IMAGE" >/dev/null 2>&1; then
|
||||||
|
echo "Automatic rollback unavailable: ${GEOINTEL_PREVIOUS_IMAGE} does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "Rolling back to ${GEOINTEL_PREVIOUS_IMAGE}..."
|
||||||
|
start_image "$GEOINTEL_PREVIOUS_IMAGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
docker compose -f docker-compose.unraid.yml config >/dev/null
|
||||||
|
|
||||||
|
current_image_id="$(docker inspect --format '{{.Image}}' geointel 2>/dev/null || true)"
|
||||||
|
if [ -n "$current_image_id" ] && docker image inspect "$current_image_id" >/dev/null 2>&1; then
|
||||||
|
current_revision="$(
|
||||||
|
docker image inspect \
|
||||||
|
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
|
||||||
|
"$current_image_id" 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
if [ -n "$current_revision" ] && [ "$current_revision" != "<no value>" ] && [ "$current_revision" != "unknown" ]; then
|
||||||
|
docker tag "$current_image_id" "${GEOINTEL_IMAGE_REPOSITORY}:${current_revision}"
|
||||||
|
fi
|
||||||
|
docker tag "$current_image_id" "$GEOINTEL_PREVIOUS_IMAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
||||||
|
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
||||||
|
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
||||||
|
-f deploy/unraid/Dockerfile.all-in-one \
|
||||||
|
-t "$GEOINTEL_RELEASE_IMAGE" \
|
||||||
|
-t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \
|
||||||
|
.
|
||||||
|
|
||||||
|
if ! start_image "$GEOINTEL_RELEASE_IMAGE"; then
|
||||||
|
rollback_previous || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x scripts/live_migration_smoke.sh ]; then
|
||||||
|
if ! LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh; then
|
||||||
|
rollback_previous || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x scripts/verify_browser_runtime.sh ]; then
|
||||||
|
if ! bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"; then
|
||||||
|
rollback_previous || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployed immutable image ${GEOINTEL_RELEASE_IMAGE}."
|
||||||
|
docker image inspect \
|
||||||
|
--format 'revision={{index .Config.Labels "org.opencontainers.image.revision"}} created={{index .Config.Labels "org.opencontainers.image.created"}}' \
|
||||||
|
"$GEOINTEL_RELEASE_IMAGE"
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
<Shell>bash</Shell>
|
<Shell>bash</Shell>
|
||||||
<Privileged>false</Privileged>
|
<Privileged>false</Privileged>
|
||||||
<Support>http://192.168.10.150:1202</Support>
|
<Support>http://192.168.10.150:1202</Support>
|
||||||
<Project>GeoIntel Kempen</Project>
|
<Project>GeoIntel Belgium and North Sea</Project>
|
||||||
<Overview>GeoIntel all-in-one runs the complete GeoIntel Kempen V1 stack in one Docker container: embedded PostGIS, FastAPI backend, nginx frontend and MapLibre UI. Use docker-compose.unraid.yml or this template so the web port, storage path and database path can be edited from Unraid.</Overview>
|
<Overview>GeoIntel all-in-one runs the Belgium and Belgian North Sea workbench in one Docker container: embedded PostGIS, FastAPI backend, nginx frontend and MapLibre UI. Use docker-compose.unraid.yml or this template so the web port, storage path and database path can be edited from Unraid.</Overview>
|
||||||
<Category>Productivity: Tools: GIS:</Category>
|
<Category>Productivity: Tools: GIS:</Category>
|
||||||
<WebUI>http://[IP]:[PORT:80]/</WebUI>
|
<WebUI>http://[IP]:[PORT:80]/</WebUI>
|
||||||
<TemplateURL>deploy/unraid/geointel-unraid-template.xml</TemplateURL>
|
<TemplateURL>deploy/unraid/geointel-unraid-template.xml</TemplateURL>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<Config Name="Postgres User" Target="GEOINTEL_POSTGRES_USER" Default="geointel" Mode="" Description="Embedded PostGIS database user." Type="Variable" Display="advanced" Required="true" Mask="false">geointel</Config>
|
<Config Name="Postgres User" Target="GEOINTEL_POSTGRES_USER" Default="geointel" Mode="" Description="Embedded PostGIS database user." Type="Variable" Display="advanced" Required="true" Mask="false">geointel</Config>
|
||||||
<Config Name="Postgres Password" Target="GEOINTEL_POSTGRES_PASSWORD" Default="change-me-before-shared-use" Mode="" Description="Embedded PostGIS database password. Change before shared use." Type="Variable" Display="advanced" Required="true" Mask="true">change-me-before-shared-use</Config>
|
<Config Name="Postgres Password" Target="GEOINTEL_POSTGRES_PASSWORD" Default="change-me-before-shared-use" Mode="" Description="Embedded PostGIS database password. Change before shared use." Type="Variable" Display="advanced" Required="true" Mask="true">change-me-before-shared-use</Config>
|
||||||
<Config Name="CORS Origins" Target="GEOINTEL_CORS_ORIGINS" Default="http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202" Mode="" Description="Comma-separated browser origins allowed to call the backend directly." Type="Variable" Display="advanced" Required="false" Mask="false">http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202</Config>
|
<Config Name="CORS Origins" Target="GEOINTEL_CORS_ORIGINS" Default="http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202" Mode="" Description="Comma-separated browser origins allowed to call the backend directly." Type="Variable" Display="advanced" Required="false" Mask="false">http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202</Config>
|
||||||
<Config Name="Max Upload MB" Target="GEOINTEL_MAX_UPLOAD_MB" Default="500" Mode="" Description="Maximum upload size in MiB enforced by the backend settings." Type="Variable" Display="advanced" Required="true" Mask="false">500</Config>
|
<Config Name="Max Upload MB" Target="GEOINTEL_MAX_UPLOAD_MB" Default="500" Mode="" Description="Maximum upload size in MiB enforced consistently by nginx and the backend (1-2048)." Type="Variable" Display="advanced" Required="true" Mask="false">500</Config>
|
||||||
<Config Name="Official Orthophoto Acquisition" Target="ORTHOPHOTO_ENABLED" Default="true" Mode="" Description="Allow explicit bounded map selections to request the official Digitaal Vlaanderen orthophoto WMS." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
|
<Config Name="Official Orthophoto Acquisition" Target="ORTHOPHOTO_ENABLED" Default="true" Mode="" Description="Allow explicit bounded map selections to request the official Digitaal Vlaanderen orthophoto WMS." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
|
||||||
<Config Name="Orthophoto WMS URL" Target="ORTHOPHOTO_WMS_URL" Default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms" Mode="" Description="Official Digitaal Vlaanderen most-recent winter orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/OMWRGBMRVL/wms</Config>
|
<Config Name="Orthophoto WMS URL" Target="ORTHOPHOTO_WMS_URL" Default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms" Mode="" Description="Official Digitaal Vlaanderen most-recent winter orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/OMWRGBMRVL/wms</Config>
|
||||||
<Config Name="Orthophoto Resolution (m)" Target="ORTHOPHOTO_RESOLUTION_M" Default="1.0" Mode="" Description="Requested analysis sampling in metres per pixel. Keep at 1.0 for the active building model profile." Type="Variable" Display="advanced" Required="true" Mask="false">1.0</Config>
|
<Config Name="Orthophoto Resolution (m)" Target="ORTHOPHOTO_RESOLUTION_M" Default="1.0" Mode="" Description="Requested analysis sampling in metres per pixel. Keep at 1.0 for the active building model profile." Type="Variable" Display="advanced" Required="true" Mask="false">1.0</Config>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<Config Name="MDK Probe Timeout Seconds" Target="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS" Default="20" Mode="" Description="Maximum wait for one read-only MDK GetCapabilities request." Type="Variable" Display="advanced" Required="true" Mask="false">20</Config>
|
<Config Name="MDK Probe Timeout Seconds" Target="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS" Default="20" Mode="" Description="Maximum wait for one read-only MDK GetCapabilities request." Type="Variable" Display="advanced" Required="true" Mask="false">20</Config>
|
||||||
<Config Name="Official Thematic Raster Acquisition" Target="THEMATIC_RASTER_ENABLED" Default="true" Mode="" Description="Allow bounded official Departement Omgeving rasters for space, population, accessibility and services." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
|
<Config Name="Official Thematic Raster Acquisition" Target="THEMATIC_RASTER_ENABLED" Default="true" Mode="" Description="Allow bounded official Departement Omgeving rasters for space, population, accessibility and services." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
|
||||||
<Config Name="Thematic Raster WCS URL" Target="THEMATIC_RASTER_WCS_URL" Default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs" Mode="" Description="Official public MercatorNet WCS endpoint. Product identifiers remain server allowlisted." Type="Variable" Display="advanced" Required="true" Mask="false">https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs</Config>
|
<Config Name="Thematic Raster WCS URL" Target="THEMATIC_RASTER_WCS_URL" Default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs" Mode="" Description="Official public MercatorNet WCS endpoint. Product identifiers remain server allowlisted." Type="Variable" Display="advanced" Required="true" Mask="false">https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs</Config>
|
||||||
<Config Name="Thematic Raster Maximum Side (m)" Target="THEMATIC_RASTER_MAX_SIDE_M" Default="60000" Mode="" Description="Maximum side length for one allowlisted thematic raster scope; supports the complete Kempen work area while WCS transfers remain tiled." Type="Variable" Display="advanced" Required="true" Mask="false">60000</Config>
|
<Config Name="Thematic Raster Maximum Side (m)" Target="THEMATIC_RASTER_MAX_SIDE_M" Default="60000" Mode="" Description="Maximum side length for one bounded allowlisted thematic raster request; larger national selections must remain partitioned." Type="Variable" Display="advanced" Required="true" Mask="false">60000</Config>
|
||||||
<Config Name="Thematic Raster Maximum Cells" Target="THEMATIC_RASTER_MAX_PIXELS" Default="30000000" Mode="" Description="Maximum raster cells per allowlisted thematic acquisition or selection analysis." Type="Variable" Display="advanced" Required="true" Mask="false">30000000</Config>
|
<Config Name="Thematic Raster Maximum Cells" Target="THEMATIC_RASTER_MAX_PIXELS" Default="30000000" Mode="" Description="Maximum raster cells per allowlisted thematic acquisition or selection analysis." Type="Variable" Display="advanced" Required="true" Mask="false">30000000</Config>
|
||||||
<Config Name="Local Ollama Assistant" Target="OLLAMA_ENABLED" Default="true" Mode="" Description="Enable the source-grounded GeoIntel assistant backed by Ollama on the Unraid host." Type="Variable" Display="always" Required="true" Mask="false">true</Config>
|
<Config Name="Local Ollama Assistant" Target="OLLAMA_ENABLED" Default="true" Mode="" Description="Enable the source-grounded GeoIntel assistant backed by Ollama on the Unraid host." Type="Variable" Display="always" Required="true" Mask="false">true</Config>
|
||||||
<Config Name="Ollama Base URL" Target="OLLAMA_BASE_URL" Default="http://host.docker.internal:11434" Mode="" Description="Ollama API reachable from the container. The deployment maps host.docker.internal to the Unraid host gateway." Type="Variable" Display="always" Required="true" Mask="false">http://host.docker.internal:11434</Config>
|
<Config Name="Ollama Base URL" Target="OLLAMA_BASE_URL" Default="http://host.docker.internal:11434" Mode="" Description="Ollama API reachable from the container. The deployment maps host.docker.internal to the Unraid host gateway." Type="Variable" Display="always" Required="true" Mask="false">http://host.docker.internal:11434</Config>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
|
|||||||
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
|
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
|
||||||
|
|
||||||
# Internal embedded PostGIS settings. The database is not published to the LAN.
|
# Internal embedded PostGIS settings. The database is not published to the LAN.
|
||||||
|
# Replace the placeholder with a unique secret before the first start. Production
|
||||||
|
# startup rejects empty and known-default passwords.
|
||||||
GEOINTEL_POSTGRES_DB=geointel
|
GEOINTEL_POSTGRES_DB=geointel
|
||||||
GEOINTEL_POSTGRES_USER=geointel
|
GEOINTEL_POSTGRES_USER=geointel
|
||||||
GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
|
GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
|
||||||
@@ -21,7 +23,7 @@ GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
|
|||||||
# Browser origins allowed when directly calling the backend API.
|
# Browser origins allowed when directly calling the backend API.
|
||||||
GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202
|
GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202
|
||||||
|
|
||||||
# Upload guard in MiB.
|
# Upload guard in MiB. The same 1-2048 limit is applied by nginx and FastAPI.
|
||||||
GEOINTEL_MAX_UPLOAD_MB=500
|
GEOINTEL_MAX_UPLOAD_MB=500
|
||||||
|
|
||||||
# Explicit, bounded acquisition from the official Digitaal Vlaanderen WMS.
|
# Explicit, bounded acquisition from the official Digitaal Vlaanderen WMS.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
client_max_body_size 250m;
|
client_max_body_size __GEOINTEL_MAX_UPLOAD_MB__m;
|
||||||
proxy_read_timeout 600s;
|
proxy_read_timeout 600s;
|
||||||
proxy_send_timeout 600s;
|
proxy_send_timeout 600s;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
GEOINTEL_ROLLBACK_IMAGE="${GEOINTEL_ROLLBACK_IMAGE:-geointel-all-in-one:previous}"
|
||||||
|
|
||||||
|
if ! docker image inspect "$GEOINTEL_ROLLBACK_IMAGE" >/dev/null 2>&1; then
|
||||||
|
echo "Rollback image does not exist: ${GEOINTEL_ROLLBACK_IMAGE}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting rollback image ${GEOINTEL_ROLLBACK_IMAGE} without changing persistent volumes..."
|
||||||
|
GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE" bash deploy/unraid/run-dockerman-container.sh
|
||||||
|
|
||||||
|
for attempt in $(seq 1 90); do
|
||||||
|
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
||||||
|
if [ "$status" = "healthy" ]; then
|
||||||
|
LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh
|
||||||
|
echo "Rollback completed with healthy image ${GEOINTEL_ROLLBACK_IMAGE}."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
|
||||||
|
docker logs --tail 120 geointel >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Rollback container did not become healthy." >&2
|
||||||
|
docker logs --tail 120 geointel >&2 || true
|
||||||
|
exit 1
|
||||||
@@ -12,12 +12,13 @@ if [ -f .env ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
GEOINTEL_FRONTEND_PORT="${GEOINTEL_FRONTEND_PORT:-1202}"
|
GEOINTEL_FRONTEND_PORT="${GEOINTEL_FRONTEND_PORT:-1202}"
|
||||||
|
GEOINTEL_IMAGE="${GEOINTEL_IMAGE:-geointel-all-in-one:latest}"
|
||||||
GEOINTEL_STORAGE_PATH="${GEOINTEL_STORAGE_PATH:-/mnt/user/appdata/geointel/storage}"
|
GEOINTEL_STORAGE_PATH="${GEOINTEL_STORAGE_PATH:-/mnt/user/appdata/geointel/storage}"
|
||||||
GEOINTEL_MODELS_PATH="${GEOINTEL_MODELS_PATH:-/mnt/user/appdata/geointel/models}"
|
GEOINTEL_MODELS_PATH="${GEOINTEL_MODELS_PATH:-/mnt/user/appdata/geointel/models}"
|
||||||
GEOINTEL_POSTGIS_DATA_PATH="${GEOINTEL_POSTGIS_DATA_PATH:-/mnt/user/appdata/geointel/postgres-data}"
|
GEOINTEL_POSTGIS_DATA_PATH="${GEOINTEL_POSTGIS_DATA_PATH:-/mnt/user/appdata/geointel/postgres-data}"
|
||||||
GEOINTEL_POSTGRES_DB="${GEOINTEL_POSTGRES_DB:-geointel}"
|
GEOINTEL_POSTGRES_DB="${GEOINTEL_POSTGRES_DB:-geointel}"
|
||||||
GEOINTEL_POSTGRES_USER="${GEOINTEL_POSTGRES_USER:-geointel}"
|
GEOINTEL_POSTGRES_USER="${GEOINTEL_POSTGRES_USER:-geointel}"
|
||||||
GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-geointel}"
|
GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-}"
|
||||||
GEOINTEL_CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-http://localhost:${GEOINTEL_FRONTEND_PORT},http://127.0.0.1:${GEOINTEL_FRONTEND_PORT},http://192.168.10.150:${GEOINTEL_FRONTEND_PORT}}"
|
GEOINTEL_CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-http://localhost:${GEOINTEL_FRONTEND_PORT},http://127.0.0.1:${GEOINTEL_FRONTEND_PORT},http://192.168.10.150:${GEOINTEL_FRONTEND_PORT}}"
|
||||||
GEOINTEL_MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-500}"
|
GEOINTEL_MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-500}"
|
||||||
ORTHOPHOTO_ENABLED="${ORTHOPHOTO_ENABLED:-true}"
|
ORTHOPHOTO_ENABLED="${ORTHOPHOTO_ENABLED:-true}"
|
||||||
@@ -101,6 +102,39 @@ OLLAMA_TIMEOUT_SECONDS="${OLLAMA_TIMEOUT_SECONDS:-120}"
|
|||||||
OLLAMA_MAX_OUTPUT_TOKENS="${OLLAMA_MAX_OUTPUT_TOKENS:-1200}"
|
OLLAMA_MAX_OUTPUT_TOKENS="${OLLAMA_MAX_OUTPUT_TOKENS:-1200}"
|
||||||
OLLAMA_CONTEXT_TOKENS="${OLLAMA_CONTEXT_TOKENS:-16384}"
|
OLLAMA_CONTEXT_TOKENS="${OLLAMA_CONTEXT_TOKENS:-16384}"
|
||||||
|
|
||||||
|
validate_runtime_config() {
|
||||||
|
case "$GEOINTEL_FRONTEND_PORT" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "GEOINTEL_FRONTEND_PORT must be a whole number." >&2
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "$GEOINTEL_FRONTEND_PORT" -lt 1 ] || [ "$GEOINTEL_FRONTEND_PORT" -gt 65535 ]; then
|
||||||
|
echo "GEOINTEL_FRONTEND_PORT must be between 1 and 65535." >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$GEOINTEL_MAX_UPLOAD_MB" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "GEOINTEL_MAX_UPLOAD_MB must be a whole number." >&2
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "$GEOINTEL_MAX_UPLOAD_MB" -lt 1 ] || [ "$GEOINTEL_MAX_UPLOAD_MB" -gt 2048 ]; then
|
||||||
|
echo "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048." >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$GEOINTEL_POSTGRES_PASSWORD" in
|
||||||
|
''|geointel|postgres|password|changeme|change-me-before-shared-use)
|
||||||
|
echo "Refusing deployment with an empty or known-default PostGIS password." >&2
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
docker image inspect "$GEOINTEL_IMAGE" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
install_dockerman_metadata() {
|
install_dockerman_metadata() {
|
||||||
if [ -d /boot/config/plugins/dockerMan ]; then
|
if [ -d /boot/config/plugins/dockerMan ]; then
|
||||||
mkdir -p /boot/config/plugins/dockerMan/templates-user /boot/config/plugins/dockerMan/images
|
mkdir -p /boot/config/plugins/dockerMan/templates-user /boot/config/plugins/dockerMan/images
|
||||||
@@ -125,6 +159,7 @@ migrate_compose_volume_if_needed() {
|
|||||||
cp -a "${compose_volume_path}/." "$GEOINTEL_POSTGIS_DATA_PATH/"
|
cp -a "${compose_volume_path}/." "$GEOINTEL_POSTGIS_DATA_PATH/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_runtime_config
|
||||||
install_dockerman_metadata
|
install_dockerman_metadata
|
||||||
|
|
||||||
docker compose down --remove-orphans || true
|
docker compose down --remove-orphans || true
|
||||||
@@ -233,6 +268,6 @@ docker run -d \
|
|||||||
-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \
|
-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \
|
||||||
-v "${GEOINTEL_STORAGE_PATH}:/app/storage" \
|
-v "${GEOINTEL_STORAGE_PATH}:/app/storage" \
|
||||||
-v "${GEOINTEL_MODELS_PATH}:/app/models" \
|
-v "${GEOINTEL_MODELS_PATH}:/app/models" \
|
||||||
geointel-all-in-one:latest
|
"$GEOINTEL_IMAGE"
|
||||||
|
|
||||||
docker ps --filter name=geointel
|
docker ps --filter name=geointel
|
||||||
|
|||||||
@@ -40,6 +40,11 @@
|
|||||||
run reused the same project and dataset identifiers. Browser acceptance then
|
run reused the same project and dataset identifiers. Browser acceptance then
|
||||||
exposed and closed a map guard that had prevented coverage-only selection
|
exposed and closed a map guard that had prevented coverage-only selection
|
||||||
when the chosen theme was honestly unavailable.
|
when the chosen theme was honestly unavailable.
|
||||||
|
- RC-5 deployment hardening started by centralizing Tower release execution,
|
||||||
|
moving build identity behind expensive dependency layers, retaining
|
||||||
|
immutable commit images plus a `previous` rollback target, adding automatic
|
||||||
|
rollback and an isolated fresh-install smoke, and failing closed on default
|
||||||
|
database secrets or inconsistent upload limits.
|
||||||
|
|
||||||
- Froze the RC geography as all Belgian land plus the separately labelled
|
- Froze the RC geography as all Belgian land plus the separately labelled
|
||||||
territorial sea, EEZ and continental shelf.
|
territorial sea, EEZ and continental shelf.
|
||||||
|
|||||||
@@ -34,59 +34,9 @@ git reset --hard "origin/$REMOTE_BRANCH"
|
|||||||
git branch -M "$REMOTE_BRANCH"
|
git branch -M "$REMOTE_BRANCH"
|
||||||
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
|
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
|
||||||
|
|
||||||
if [ -f .env ]; then
|
FRONTEND_URL="$FRONTEND_URL" \
|
||||||
set -a
|
DEPLOY_GEOINTEL_INSTALL_AI="${DEPLOY_GEOINTEL_INSTALL_AI:-}" \
|
||||||
. ./.env
|
bash deploy/unraid/deploy-release.sh
|
||||||
set +a
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
|
|
||||||
GEOINTEL_INSTALL_AI="$DEPLOY_GEOINTEL_INSTALL_AI"
|
|
||||||
fi
|
|
||||||
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
|
|
||||||
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
|
|
||||||
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
|
|
||||||
docker compose -f docker-compose.unraid.yml config >/dev/null
|
|
||||||
docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
|
||||||
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
|
||||||
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
|
||||||
-f deploy/unraid/Dockerfile.all-in-one \
|
|
||||||
-t geointel-all-in-one:latest \
|
|
||||||
.
|
|
||||||
bash deploy/unraid/run-dockerman-container.sh
|
|
||||||
|
|
||||||
wait_for_geointel_health() {
|
|
||||||
local status=''
|
|
||||||
for attempt in $(seq 1 90); do
|
|
||||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
|
||||||
if [ "$status" = 'healthy' ]; then
|
|
||||||
echo "GeoIntel container is healthy after attempt $attempt."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [ "$status" = 'unhealthy' ] || [ "$status" = 'exited' ] || [ "$status" = 'dead' ]; then
|
|
||||||
echo "GeoIntel container entered terminal state: $status" >&2
|
|
||||||
docker logs --tail 120 geointel >&2 || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2
|
|
||||||
docker logs --tail 120 geointel >&2 || true
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Container startup owns the migration. Waiting here prevents the validation
|
|
||||||
# smoke from racing a concurrent `alembic upgrade head` against the same DB.
|
|
||||||
wait_for_geointel_health
|
|
||||||
|
|
||||||
if [ -x scripts/live_migration_smoke.sh ]; then
|
|
||||||
LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -x scripts/verify_browser_runtime.sh ]; then
|
|
||||||
bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"
|
|
||||||
fi
|
|
||||||
'@
|
'@
|
||||||
|
|
||||||
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||||
|
|||||||
+3
-54
@@ -35,58 +35,7 @@ git fetch origin "$REMOTE_BRANCH"
|
|||||||
git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH"
|
git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH"
|
||||||
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
|
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
|
||||||
|
|
||||||
if [ -f .env ]; then
|
FRONTEND_URL="$FRONTEND_URL" \
|
||||||
set -a
|
DEPLOY_GEOINTEL_INSTALL_AI="${DEPLOY_GEOINTEL_INSTALL_AI:-}" \
|
||||||
# shellcheck disable=SC1091
|
bash deploy/unraid/deploy-release.sh
|
||||||
. ./.env
|
|
||||||
set +a
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
|
|
||||||
GEOINTEL_INSTALL_AI="$DEPLOY_GEOINTEL_INSTALL_AI"
|
|
||||||
fi
|
|
||||||
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
|
|
||||||
GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"
|
|
||||||
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
|
|
||||||
docker compose -f docker-compose.unraid.yml config >/dev/null
|
|
||||||
docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
|
||||||
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
|
||||||
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
|
||||||
-f deploy/unraid/Dockerfile.all-in-one \
|
|
||||||
-t geointel-all-in-one:latest \
|
|
||||||
.
|
|
||||||
bash deploy/unraid/run-dockerman-container.sh
|
|
||||||
|
|
||||||
wait_for_geointel_health() {
|
|
||||||
local status=''
|
|
||||||
for attempt in $(seq 1 90); do
|
|
||||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
|
|
||||||
if [[ "$status" == 'healthy' ]]; then
|
|
||||||
echo "GeoIntel container is healthy after attempt $attempt."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ "$status" == 'unhealthy' || "$status" == 'exited' || "$status" == 'dead' ]]; then
|
|
||||||
echo "GeoIntel container entered terminal state: $status" >&2
|
|
||||||
docker logs --tail 120 geointel >&2 || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2
|
|
||||||
docker logs --tail 120 geointel >&2 || true
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Container startup owns the migration. Waiting here prevents the validation
|
|
||||||
# smoke from racing a concurrent `alembic upgrade head` against the same DB.
|
|
||||||
wait_for_geointel_health
|
|
||||||
|
|
||||||
if [[ -x scripts/live_migration_smoke.sh ]]; then
|
|
||||||
LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -x scripts/verify_browser_runtime.sh ]]; then
|
|
||||||
bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"
|
|
||||||
fi
|
|
||||||
REMOTE_SCRIPT
|
REMOTE_SCRIPT
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ ${PYTHON_BIN} -m compileall backend/app
|
|||||||
(cd frontend && npm run typecheck)
|
(cd frontend && npm run typecheck)
|
||||||
(cd frontend && npm run build)
|
(cd frontend && npm run build)
|
||||||
bash -n scripts/live_migration_smoke.sh
|
bash -n scripts/live_migration_smoke.sh
|
||||||
|
bash -n scripts/deploy_tower.sh
|
||||||
|
bash -n scripts/verify_release_fresh_install.sh
|
||||||
|
bash -n deploy/unraid/all-in-one-start.sh
|
||||||
|
bash -n deploy/unraid/run-dockerman-container.sh
|
||||||
|
bash -n deploy/unraid/deploy-release.sh
|
||||||
|
bash -n deploy/unraid/rollback-dockerman-container.sh
|
||||||
bash -n scripts/verify_browser_runtime.sh
|
bash -n scripts/verify_browser_runtime.sh
|
||||||
bash -n scripts/verify_demo_export_workflow.sh
|
bash -n scripts/verify_demo_export_workflow.sh
|
||||||
bash -n scripts/verify_demo_raster_workflow.sh
|
bash -n scripts/verify_demo_raster_workflow.sh
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE="${1:-geointel-all-in-one:latest}"
|
||||||
|
NAME="geointel-fresh-smoke-$(date +%s)-$$"
|
||||||
|
ROOT="$(mktemp -d "${TMPDIR:-/tmp}/geointel-fresh-smoke.XXXXXX")"
|
||||||
|
PASSWORD="fresh-smoke-$(cat /proc/sys/kernel/random/uuid)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||||||
|
case "$ROOT" in
|
||||||
|
"${TMPDIR:-/tmp}"/geointel-fresh-smoke.*)
|
||||||
|
rm -rf -- "$ROOT"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Refusing to remove unexpected smoke path: ${ROOT}" >&2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
docker image inspect "$IMAGE" >/dev/null
|
||||||
|
mkdir -p "$ROOT/postgres-data" "$ROOT/storage" "$ROOT/models"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name "$NAME" \
|
||||||
|
-p 127.0.0.1::80 \
|
||||||
|
-e GEOINTEL_POSTGRES_DB=geointel_fresh_smoke \
|
||||||
|
-e GEOINTEL_POSTGRES_USER=geointel_fresh_smoke \
|
||||||
|
-e GEOINTEL_POSTGRES_PASSWORD="$PASSWORD" \
|
||||||
|
-e GEOINTEL_MAX_UPLOAD_MB=500 \
|
||||||
|
-e OLLAMA_ENABLED=false \
|
||||||
|
-v "$ROOT/postgres-data:/var/lib/postgresql/data" \
|
||||||
|
-v "$ROOT/storage:/app/storage" \
|
||||||
|
-v "$ROOT/models:/app/models" \
|
||||||
|
"$IMAGE" >/dev/null
|
||||||
|
|
||||||
|
for attempt in $(seq 1 90); do
|
||||||
|
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$NAME" 2>/dev/null || true)"
|
||||||
|
if [ "$status" = "healthy" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
|
||||||
|
docker logs --tail 120 "$NAME" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${status:-}" != "healthy" ]; then
|
||||||
|
echo "Fresh-install container did not become healthy." >&2
|
||||||
|
docker logs --tail 120 "$NAME" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
host_port="$(docker port "$NAME" 80/tcp | awk -F: 'NR == 1 {print $NF}')"
|
||||||
|
curl -fsS "http://127.0.0.1:${host_port}/health/ready" >/dev/null
|
||||||
|
curl -fsS "http://127.0.0.1:${host_port}/api/v1/system/capabilities" >/dev/null
|
||||||
|
docker exec "$NAME" python -m alembic heads
|
||||||
|
|
||||||
|
echo "Fresh install passed for ${IMAGE} on isolated storage ${ROOT}."
|
||||||
Reference in New Issue
Block a user