feat(deploy): Faza A Infisical runtime migration
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) <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -44,7 +44,8 @@ ENV NODE_ENV=production
|
|||||||
ENV TZ=Europe/Bucharest
|
ENV TZ=Europe/Bucharest
|
||||||
|
|
||||||
# Install system deps + create user in a single layer
|
# 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 \
|
&& addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 nextjs
|
&& 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 /app/public ./public
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
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
|
USER nextjs
|
||||||
|
|
||||||
@@ -60,4 +63,5 @@ EXPOSE 3000
|
|||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
+6
-62
@@ -13,68 +13,12 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV}
|
# Infisical runtime bootstrap (all app secrets fetched from /architools at boot via docker-entrypoint.sh)
|
||||||
# Database
|
- INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID}
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
- INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET}
|
||||||
# MinIO
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
- MINIO_ENDPOINT=${MINIO_ENDPOINT}
|
- PORT=3000
|
||||||
- MINIO_PORT=${MINIO_PORT}
|
- HOSTNAME=0.0.0.0
|
||||||
- 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}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
dwg2dxf:
|
dwg2dxf:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
Executable
+68
@@ -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("\\$\\{/(?<n>[A-Z0-9_]+)\\}"; ($rootMap[.n] // ("${/" + .n + "}")))
|
||||||
|
| gsub("\\$\\{(?<n>[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 "$@"
|
||||||
Reference in New Issue
Block a user