diff --git a/apps/frontend/app/providers.tsx b/apps/frontend/app/providers.tsx
index 1c5efc3..a659759 100644
--- a/apps/frontend/app/providers.tsx
+++ b/apps/frontend/app/providers.tsx
@@ -9,12 +9,12 @@ import { AppToastProvider } from '@/components/id/toast-provider';
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
-
-
+
+
+
{children}
-
-
+
+
);
}
diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts
index 9efc54e..ca53ac5 100644
--- a/apps/frontend/lib/api.ts
+++ b/apps/frontend/lib/api.ts
@@ -680,8 +680,6 @@ export function buildSystemSettingPayload(setting: Pick {
let gatewayReadyResolved = false;
let gatewayReadyPromise: Promise | null = null;
+let stableGatewayPromise: Promise | null = null;
let gatewayReadyAt = 0;
let activeApiRequests = 0;
const apiRequestWaiters: Array<() => void> = [];
@@ -703,11 +702,13 @@ const apiRequestWaiters: Array<() => void> = [];
export function resetApiGatewayWarmup() {
gatewayReadyResolved = false;
gatewayReadyPromise = null;
+ stableGatewayPromise = null;
}
export function invalidateApiGatewayReady() {
gatewayReadyResolved = false;
gatewayReadyAt = 0;
+ stableGatewayPromise = null;
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent(API_NOT_READY_EVENT));
}
@@ -745,26 +746,34 @@ export function subscribeApiNotReady(listener: () => void): () => void {
/** Несколько подряд успешных /health через nginx — защита от «один OK, потом 502». */
export async function waitForStableGateway(stableProbes = 3): Promise {
if (typeof window === 'undefined') return;
+ if (gatewayReadyResolved && stableProbes <= 1) return;
+ if (stableGatewayPromise) return stableGatewayPromise;
- gatewayReadyResolved = false;
- gatewayReadyAt = 0;
+ stableGatewayPromise = (async () => {
+ gatewayReadyResolved = false;
+ gatewayReadyAt = 0;
- let streak = 0;
- for (let attempt = 0; attempt < GATEWAY_STABLE_MAX_ROUNDS && streak < stableProbes; attempt += 1) {
- if (await probeGatewayAlive()) {
- streak += 1;
- if (streak >= stableProbes) {
- markApiGatewayReady();
- return;
+ let streak = 0;
+ for (let attempt = 0; attempt < GATEWAY_STABLE_MAX_ROUNDS && streak < stableProbes; attempt += 1) {
+ if (await probeGatewayAlive()) {
+ streak += 1;
+ if (streak >= stableProbes) {
+ markApiGatewayReady();
+ return;
+ }
+ await sleep(GATEWAY_STABLE_PROBE_INTERVAL_MS);
+ } else {
+ streak = 0;
+ await sleep(Math.min(900 + attempt * 300, 2800));
}
- await sleep(GATEWAY_STABLE_PROBE_INTERVAL_MS);
- } else {
- streak = 0;
- await sleep(Math.min(900 + attempt * 300, 2800));
}
- }
- throw new ApiError('Сервер API временно недоступен. Попробуйте обновить страницу.', 503, 'GATEWAY_UNAVAILABLE');
+ throw new ApiError('Сервер API временно недоступен. Попробуйте обновить страницу.', 503, 'GATEWAY_UNAVAILABLE');
+ })().finally(() => {
+ stableGatewayPromise = null;
+ });
+
+ return stableGatewayPromise;
}
async function acquireApiSlot(): Promise {
@@ -799,23 +808,7 @@ async function probeGatewayAlive(): Promise {
}
}
-async function waitForGatewayAlive(): Promise {
- if (gatewayReadyResolved) return;
-
- for (let attempt = 0; attempt < GATEWAY_PROBE_MAX_ATTEMPTS; attempt += 1) {
- if (await probeGatewayAlive()) {
- markApiGatewayReady();
- return;
- }
- if (attempt + 1 < GATEWAY_PROBE_MAX_ATTEMPTS) {
- await sleep(Math.min(GATEWAY_PROBE_BASE_DELAY_MS + attempt * 280, 2800));
- }
- }
-
- throw new ApiError('Сервер API временно недоступен. Попробуйте обновить страницу.', 503, 'GATEWAY_UNAVAILABLE');
-}
-
-/** Один общий прогрев: все apiFetch ждут успешный /health, без раннего «fail-open». */
+/** Один общий прогрев: все apiFetch ждут стабильный /health через nginx. */
export async function ensureApiGatewayReady(force = false): Promise {
if (typeof window === 'undefined') return;
if (gatewayReadyResolved && !force) return;
@@ -823,9 +816,12 @@ export async function ensureApiGatewayReady(force = false): Promise {
if (force) {
gatewayReadyResolved = false;
+ gatewayReadyAt = 0;
+ stableGatewayPromise = null;
}
- gatewayReadyPromise = waitForGatewayAlive().finally(() => {
+ const probes = force ? 2 : 1;
+ gatewayReadyPromise = waitForStableGateway(probes).finally(() => {
gatewayReadyPromise = null;
});
@@ -848,7 +844,7 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise/dev/null | grep -q '\"status\"' || exit 1",
+ ]
+ interval: 15s
timeout: 5s
- retries: 3
-
- # После recreate api-gateway nginx держит stale DNS upstream → 502. Watcher
- # делает nginx -s reload когда gateway снова поднялся (~55 с на Nest bootstrap).
- nginx-gateway-sync:
- profiles: ["proxy"]
- image: docker:27-cli
- container_name: lendry-id-nginx-gateway-sync
- restart: unless-stopped
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock:ro
- - ./infra/nginx-gateway-sync/entrypoint.sh:/entrypoint.sh:ro
- depends_on:
- nginx:
- condition: service_started
- entrypoint: ["/bin/sh", "/entrypoint.sh"]
+ retries: 5
+ start_period: 30s
volumes:
postgres_data:
diff --git a/infra/nginx-gateway-sync/entrypoint.sh b/infra/nginx-gateway-sync/entrypoint.sh
deleted file mode 100644
index ee2ca8d..0000000
--- a/infra/nginx-gateway-sync/entrypoint.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/sh
-set -eu
-
-NGINX_CONTAINER="${NGINX_CONTAINER:-lendry-id-nginx}"
-GATEWAY_CONTAINER="${GATEWAY_CONTAINER:-lendry-id-api-gateway}"
-BOOT_WAIT_SEC="${BOOT_WAIT_SEC:-55}"
-GATEWAY_HEALTH_URL="http://${GATEWAY_CONTAINER}:3000/health"
-
-reload_nginx() {
- if docker exec "$NGINX_CONTAINER" nginx -s reload 2>/dev/null; then
- echo "[nginx-gateway-sync] nginx reload OK"
- else
- echo "[nginx-gateway-sync] nginx reload failed — пробуем restart"
- docker restart "$NGINX_CONTAINER" >/dev/null 2>&1 || true
- fi
-}
-
-verify_gateway() {
- if docker exec "$NGINX_CONTAINER" wget -qO- --timeout=5 "$GATEWAY_HEALTH_URL" 2>/dev/null \
- | grep -q '"status"'; then
- echo "[nginx-gateway-sync] nginx → ${GATEWAY_CONTAINER}:3000/health OK"
- else
- echo "[nginx-gateway-sync] предупреждение: nginx пока не достучался до ${GATEWAY_HEALTH_URL}"
- fi
-}
-
-on_gateway_start() {
- echo "[nginx-gateway-sync] ${GATEWAY_CONTAINER} перезапущен, ждём Nest (${BOOT_WAIT_SEC}s)..."
- sleep "$BOOT_WAIT_SEC"
- reload_nginx
- sleep 2
- verify_gateway
-}
-
-echo "[nginx-gateway-sync] слежение за ${GATEWAY_CONTAINER} (boot wait ${BOOT_WAIT_SEC}s)..."
-
-# Первый запуск стека: gateway уже healthy, но nginx мог стартовать раньше — синхронизируем.
-(
- sleep "$BOOT_WAIT_SEC"
- reload_nginx
- sleep 2
- verify_gateway
-) &
-
-docker events \
- --filter "container=${GATEWAY_CONTAINER}" \
- --filter event=start \
- --format '{{.Time}}' | while read -r _; do
- on_gateway_start
-done
diff --git a/install.sh b/install.sh
index 1fbf4ec..0a3cfb7 100644
--- a/install.sh
+++ b/install.sh
@@ -10,7 +10,7 @@
set -euo pipefail
-SCRIPT_VERSION="2.4.2"
+SCRIPT_VERSION="2.5.0"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"
@@ -426,30 +426,27 @@ recreate_docker_nginx() {
compose_uses_proxy_profile || return 0
build_compose_stack_cmd
log "Пересоздание контейнера lendry-id-nginx (подхват конфигов из ./nginx/conf.d)..."
- docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate nginx nginx-gateway-sync
+ docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate nginx
wait_for_service "nginx" 60
}
-ensure_nginx_gateway_sync() {
- compose_uses_proxy_profile || return 0
- build_compose_stack_cmd
- log "Запуск nginx-gateway-sync (авто-reload Nginx после пересборки api-gateway)..."
- docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d nginx-gateway-sync
-}
-
post_deploy_proxy_verify() {
local strict="${1:-0}"
compose_uses_proxy_profile || return 0
- wait_for_service "nginx" 30
- ensure_nginx_gateway_sync
+ wait_for_service "nginx" 60
wait_for_api_grpc_ready 90 || {
[[ "$strict" -eq 1 ]] && fail "api-gateway → sso-core gRPC не готов (/health/ready). Проверьте: docker compose logs sso-core api-gateway"
warn "api-gateway → sso-core gRPC не готов (/health/ready)"
}
- sync_nginx_after_api_gateway_restart
+ log "Проверка Docker Nginx → api-gateway (DNS + /idp-api)..."
+ reload_docker_nginx
+ sleep 2
verify_nginx_reaches_api_gateway 24 5 || {
- [[ "$strict" -eq 1 ]] && fail "Docker Nginx не достучался до api-gateway:3000 — см. docker compose logs nginx api-gateway"
- warn "Docker Nginx → api-gateway: проверка не прошла"
+ reload_docker_nginx
+ verify_nginx_reaches_api_gateway 12 5 || {
+ [[ "$strict" -eq 1 ]] && fail "Docker Nginx не достучался до api-gateway:3000 — см. docker compose logs nginx api-gateway"
+ warn "Docker Nginx → api-gateway: проверка не прошла"
+ }
}
verify_nginx_reaches_media_ws || true
if [[ "$(env_get USE_NGINX_SSL false)" == "true" ]]; then
@@ -460,6 +457,32 @@ post_deploy_proxy_verify() {
fi
}
+# Поэтапный запуск: nginx стартует ПОСЛЕ healthy api-gateway — без 502 на первом запросе.
+deploy_phased_stack() {
+ log "Фаза 1/5: PostgreSQL, Redis, RabbitMQ, MinIO, LDAP..."
+ compose_build_and_up postgres redis rabbitmq minio ldap-auth
+ wait_for_service "ldap-auth" 90
+
+ log "Фаза 2/5: sso-core..."
+ SKIP_DOCKER_PRUNE=1
+ compose_build_and_up sso-core
+ wait_for_service "sso-core" 120
+
+ log "Фаза 3/5: api-gateway..."
+ compose_build_and_up api-gateway
+ wait_for_service "api-gateway" 90
+ wait_for_api_grpc_ready 90 || warn "gRPC api-gateway → sso-core не готов — API может отдавать 502"
+
+ log "Фаза 4/5: frontend, docs, media-ws..."
+ compose_build_and_up frontend docs media-ws
+ wait_for_service "frontend" 90
+ wait_for_service "docs" 60
+
+ log "Фаза 5/5: Docker Nginx (после готовности backend)..."
+ compose_build_and_up nginx
+ post_deploy_proxy_verify 1
+}
+
prepare_compose_stack() {
resolve_nginx_mode_and_ports
write_compose_override
@@ -2050,10 +2073,6 @@ nginx_upstream_var() {
printf 'idp_up_%s' "$(printf '%s' "$1" | tr -c 'A-Za-z0-9' '_')"
}
-nginx_api_uses_static_ip() {
- return 1
-}
-
# proxy_pass к КОРНЮ апстрима ($request_uri сохраняет путь и query).
# docker: resolver + переменная (перерезолв DNS после recreate api-gateway).
# host: статический proxy_pass на 127.0.0.1.
@@ -2904,16 +2923,18 @@ deploy_stack() {
verify_postgres_auth
log "Сборка и запуск контейнеров (10–20 минут при первом запуске)..."
- compose_build_and_up
- recreate_host_port_services
- ensure_host_upstreams
-
- wait_for_service "sso-core" 120
- wait_for_service "api-gateway" 90
- wait_for_api_grpc_ready 60 || warn "gRPC api-gateway → sso-core не готов — API может отдавать 502"
- wait_for_service "frontend" 60
- wait_for_service "docs" 60
- post_deploy_proxy_verify 1
+ if compose_uses_proxy_profile; then
+ deploy_phased_stack
+ else
+ compose_build_and_up
+ recreate_host_port_services
+ ensure_host_upstreams
+ wait_for_service "sso-core" 120
+ wait_for_service "api-gateway" 90
+ wait_for_api_grpc_ready 60 || warn "gRPC api-gateway → sso-core не готов — API может отдавать 502"
+ wait_for_service "frontend" 60
+ wait_for_service "docs" 60
+ fi
}
save_install_state() {
@@ -3343,6 +3364,7 @@ docker_fresh_purge() {
build_compose_stack_cmd
log "Остановка стека и удаление volumes (PostgreSQL, Redis, MinIO, RabbitMQ)..."
+ docker_cmd rm -f lendry-id-nginx-gateway-sync 2>/dev/null || true
docker_cmd "${COMPOSE_STACK_CMD[@]}" down -v --remove-orphans 2>/dev/null || true
docker_cmd compose --env-file .env down -v --remove-orphans 2>/dev/null || true
docker_cmd compose --env-file .env --profile proxy down -v --remove-orphans 2>/dev/null || true
@@ -3609,7 +3631,6 @@ action_fix_all_errors() {
esac
apply_intranet_selfsigned_runtime
recreate_docker_nginx
- ensure_nginx_gateway_sync
sync_nginx_after_api_gateway_restart
verify_nginx_reaches_api_gateway 24 5 || compose_build_and_up api-gateway
sync_nginx_after_api_gateway_restart