From 54b78c2dcf996bada34b517d3546a96d6d1775b2 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Mon, 18 May 2026 00:41:22 +0300 Subject: [PATCH] feat(deploy): Faza A Infisical runtime migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stripped 35-var environment block from docker-compose.yml to 5 bootstrap vars (INFISICAL_CLIENT_ID/SECRET, NODE_ENV, PORT, HOSTNAME). All app secrets now fetched from Infisical /architools at container boot via docker-entrypoint.sh (modeled on gis-api's pattern, INFISICAL_APP_PATH =/architools). - docker-entrypoint.sh: universal-auth login, fetch /architools + / root, expand ${/VAR} refs, export, exec CMD. Fails loud on Infisical unreachable (exit 2/3). - Dockerfile runner: added curl+jq, COPY entrypoint + chmod +x, ENTRYPOINT ["/app/docker-entrypoint.sh"] - compose: build args (NEXT_PUBLIC_*) preserved — build-time inlining into JS bundle. martin/tile-cache/tippecanoe service env blocks untouched (legacy, removed in Faza E). Rotation workflow now: Infisical UI -> ssh satra "cd /opt/architools && docker compose up -d --force-recreate architools". Never docker compose restart (does not refetch). Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile | 6 +++- docker-compose.yml | 68 ++++---------------------------------------- docker-entrypoint.sh | 68 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 63 deletions(-) create mode 100755 docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 20e11f0..1d2aa1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,8 @@ ENV NODE_ENV=production ENV TZ=Europe/Bucharest # Install system deps + create user in a single layer -RUN apk add --no-cache gdal gdal-tools ghostscript qpdf tzdata \ +# curl + jq required by docker-entrypoint.sh for Infisical runtime bootstrap +RUN apk add --no-cache gdal gdal-tools ghostscript qpdf tzdata curl jq \ && addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs @@ -53,6 +54,8 @@ RUN apk add --no-cache gdal gdal-tools ghostscript qpdf tzdata \ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh USER nextjs @@ -60,4 +63,5 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" +ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 357efe5..90ce342 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,68 +13,12 @@ services: ports: - "3000:3000" environment: - - NODE_ENV=${NODE_ENV} - # Database - - DATABASE_URL=${DATABASE_URL} - # MinIO - - MINIO_ENDPOINT=${MINIO_ENDPOINT} - - MINIO_PORT=${MINIO_PORT} - - MINIO_USE_SSL=${MINIO_USE_SSL} - - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME} - # Authentication (Authentik OIDC) - - NEXTAUTH_URL=${NEXTAUTH_URL} - - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - - AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID} - - AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET} - - AUTHENTIK_ISSUER=${AUTHENTIK_ISSUER} - # Vault encryption - - ENCRYPTION_SECRET=${ENCRYPTION_SECRET} - # ManicTime Tags.txt sync (SMB mount path) - - MANICTIME_TAGS_PATH=${MANICTIME_TAGS_PATH} - # AI Chat (set AI_PROVIDER to openai/anthropic/ollama; demo if no key) - - AI_PROVIDER=${AI_PROVIDER:-demo} - - AI_API_KEY=${AI_API_KEY:-} - - AI_MODEL=${AI_MODEL:-} - - AI_BASE_URL=${AI_BASE_URL:-} - - AI_MAX_TOKENS=${AI_MAX_TOKENS:-2048} - # Visual CoPilot (at-vim) - - VIM_URL=${VIM_URL:-} - # eTerra ANCPI (parcel-sync module) - - ETERRA_USERNAME=${ETERRA_USERNAME:-} - - ETERRA_PASSWORD=${ETERRA_PASSWORD:-} - # ANCPI ePay (CF extract ordering) - - ANCPI_USERNAME=${ANCPI_USERNAME} - - ANCPI_PASSWORD=${ANCPI_PASSWORD} - - ANCPI_BASE_URL=${ANCPI_BASE_URL} - - ANCPI_LOGIN_URL=${ANCPI_LOGIN_URL} - - ANCPI_DEFAULT_SOLICITANT_ID=${ANCPI_DEFAULT_SOLICITANT_ID} - - MINIO_BUCKET_ANCPI=${MINIO_BUCKET_ANCPI} - # Stirling PDF (local PDF tools) - - STIRLING_PDF_URL=${STIRLING_PDF_URL} - - STIRLING_PDF_API_KEY=${STIRLING_PDF_API_KEY} - # iLovePDF cloud compression (free: 250 files/month) - - ILOVEPDF_PUBLIC_KEY=${ILOVEPDF_PUBLIC_KEY:-} - # Martin vector tile server (geoportal) - - NEXT_PUBLIC_MARTIN_URL=${NEXT_PUBLIC_MARTIN_URL} - # PMTiles overview tiles — proxied through tile-cache nginx (HTTPS, no mixed-content) - - NEXT_PUBLIC_PMTILES_URL=${NEXT_PUBLIC_PMTILES_URL} - # DWG-to-DXF sidecar - - DWG2DXF_URL=${DWG2DXF_URL} - # Email notifications (Brevo REST API) - - BREVO_API_KEY=${BREVO_API_KEY} - - NOTIFICATION_FROM_EMAIL=${NOTIFICATION_FROM_EMAIL} - - NOTIFICATION_FROM_NAME=${NOTIFICATION_FROM_NAME} - - NOTIFICATION_CRON_SECRET=${NOTIFICATION_CRON_SECRET} - # Weekend Deep Sync email reports (comma-separated for multiple recipients) - - WEEKEND_SYNC_EMAIL=${WEEKEND_SYNC_EMAIL:-} - # PMTiles rebuild webhook (pmtiles-webhook systemd service on host) - - N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://10.10.10.166:9876} - # Portal-only users (comma-separated, redirected to /portal) - - PORTAL_ONLY_USERS=${PORTAL_ONLY_USERS} - # Address Book API (inter-service auth for external tools) - - ADDRESSBOOK_API_KEY=${ADDRESSBOOK_API_KEY} + # Infisical runtime bootstrap (all app secrets fetched from /architools at boot via docker-entrypoint.sh) + - INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID} + - INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET} + - NODE_ENV=${NODE_ENV:-production} + - PORT=3000 + - HOSTNAME=0.0.0.0 depends_on: dwg2dxf: condition: service_healthy diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..88c4a45 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,68 @@ +#!/bin/sh +set -e + +# Infisical runtime bootstrap. Required: +# INFISICAL_CLIENT_ID - universal-auth clientId for `architools-prod` +# INFISICAL_CLIENT_SECRET - universal-auth clientSecret (bootstrap only) +# Optional: +# INFISICAL_URL default https://infisical.beletage.ro +# INFISICAL_WORKSPACE_ID default 078c998d-43a9-420c-aec4-712011108410 +# INFISICAL_ENV default prod +# INFISICAL_APP_PATH default /architools + +if [ -n "$INFISICAL_CLIENT_ID" ] && [ -n "$INFISICAL_CLIENT_SECRET" ]; then + INFISICAL_URL="${INFISICAL_URL:-https://infisical.beletage.ro}" + INFISICAL_WORKSPACE_ID="${INFISICAL_WORKSPACE_ID:-078c998d-43a9-420c-aec4-712011108410}" + INFISICAL_ENV="${INFISICAL_ENV:-prod}" + INFISICAL_APP_PATH="${INFISICAL_APP_PATH:-/architools}" + + echo "[infisical] authenticating as architools-prod..." + AUTH_RESP=$(curl -sk --fail -m 10 -X POST "$INFISICAL_URL/api/v1/auth/universal-auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"clientId\":\"$INFISICAL_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CLIENT_SECRET\"}") || { + echo "[infisical] FATAL: universal-auth login failed"; exit 2; + } + INFISICAL_TOKEN=$(printf '%s' "$AUTH_RESP" | jq -r '.accessToken') + unset AUTH_RESP + [ -n "$INFISICAL_TOKEN" ] && [ "$INFISICAL_TOKEN" != "null" ] || { + echo "[infisical] FATAL: no accessToken in login response"; exit 2; + } + + fetch_path() { + curl -sk --fail -m 10 \ + "$INFISICAL_URL/api/v3/secrets/raw?workspaceId=$INFISICAL_WORKSPACE_ID&environment=$INFISICAL_ENV&secretPath=$1" \ + -H "Authorization: Bearer $INFISICAL_TOKEN" + } + + APP_SECRETS=$(fetch_path "$INFISICAL_APP_PATH") || { + echo "[infisical] FATAL: fetch $INFISICAL_APP_PATH failed"; exit 3; + } + ROOT_SECRETS=$(fetch_path "/") || { + echo "[infisical] FATAL: fetch / failed"; exit 3; + } + + APP_COUNT=$(printf '%s' "$APP_SECRETS" | jq '.secrets | length') + ROOT_COUNT=$(printf '%s' "$ROOT_SECRETS" | jq '.secrets | length') + echo "[infisical] fetched $APP_COUNT app secrets, $ROOT_COUNT root secrets" + + TMP=$(mktemp); trap "rm -f $TMP" EXIT + printf '%s' "$APP_SECRETS" | jq -r --argjson rootMap "$(printf '%s' "$ROOT_SECRETS" | jq '.secrets | map({(.secretKey): .secretValue}) | add')" ' + .secrets[] as $s | + ($s.secretValue + | gsub("\\$\\{/(?[A-Z0-9_]+)\\}"; ($rootMap[.n] // ("${/" + .n + "}"))) + | gsub("\\$\\{(?[A-Z0-9_]+)\\}"; ($rootMap[.n] // ("${" + .n + "}"))) + ) as $resolved | + "export " + $s.secretKey + "=" + ($resolved | @sh) + ' > "$TMP" + + unset APP_SECRETS ROOT_SECRETS INFISICAL_TOKEN + # shellcheck disable=SC1090 + . "$TMP" + rm -f "$TMP"; trap - EXIT + echo "[infisical] exported env - DATABASE_URL=${DATABASE_URL:+set} AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID:+set} MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:+set} GIS_API_URL=${GIS_API_URL:+set}" +else + echo "[infisical] INFISICAL_CLIENT_ID unset - skipping bootstrap (dev mode)" +fi + +echo "[entrypoint] Starting app: $*" +exec "$@"