From ce8463951c50e38be0d920993a5d6b425bddb92f Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Feb 2026 19:25:47 -0800 Subject: [PATCH] feat: wire rSocials to pull secrets from Infisical at startup Add entrypoint.sh that authenticates with Infisical Machine Identity and injects secrets as env vars before starting the Node.js app. Replaces individual secret env vars in docker-compose.yml with Infisical client credentials. Falls back gracefully if Infisical is unavailable. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 6 ++-- docker-compose.yml | 11 ++++--- entrypoint.sh | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index fbf620c..6680e04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,12 +39,13 @@ RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static +COPY entrypoint.sh /app/entrypoint.sh # Create data directory for zine storage RUN mkdir -p /app/data/zines && chown -R nextjs:nodejs /app/data -# Set ownership -RUN chown -R nextjs:nodejs /app +# Set ownership and make entrypoint executable +RUN chmod +x /app/entrypoint.sh && chown -R nextjs:nodejs /app USER nextjs @@ -54,4 +55,5 @@ ENV PORT=3000 ENV HOSTNAME="0.0.0.0" ENV DATA_DIR=/app/data +ENTRYPOINT ["/app/entrypoint.sh"] CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 05106da..9d6ee6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,10 +6,13 @@ services: container_name: rsocials restart: unless-stopped environment: - - GEMINI_API_KEY=${GEMINI_API_KEY} - - RUNPOD_API_KEY=${RUNPOD_API_KEY} - - RUNPOD_GEMINI_ENDPOINT_ID=${RUNPOD_GEMINI_ENDPOINT_ID:-ntqjz8cdsth42i} - - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-https://rsocials.online} + # Infisical secret injection (replaces individual secret env vars) + - INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID} + - INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET} + - INFISICAL_PROJECT_SLUG=rsocials + - INFISICAL_ENV=prod + - INFISICAL_URL=http://infisical:8080 + # Non-secret config - DATA_DIR=/app/data volumes: - zine-data:/app/data diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..88a4673 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# Infisical secret injection entrypoint +# Fetches secrets from Infisical API and injects them as env vars before starting the app. +# Required env vars: INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET +# Optional: INFISICAL_PROJECT_SLUG (default: rsocials), INFISICAL_ENV (default: prod), +# INFISICAL_URL (default: http://infisical:8080) + +set -e + +INFISICAL_URL="${INFISICAL_URL:-http://infisical:8080}" +INFISICAL_ENV="${INFISICAL_ENV:-prod}" +INFISICAL_PROJECT_SLUG="${INFISICAL_PROJECT_SLUG:-rsocials}" + +if [ -z "$INFISICAL_CLIENT_ID" ] || [ -z "$INFISICAL_CLIENT_SECRET" ]; then + echo "[infisical] No credentials set, starting without secret injection" + exec "$@" +fi + +echo "[infisical] Fetching secrets from ${INFISICAL_PROJECT_SLUG}/${INFISICAL_ENV}..." + +# Use Node.js (already in the image) for reliable JSON parsing and HTTP calls +EXPORTS=$(node -e " +const http = require('http'); +const https = require('https'); +const url = new URL(process.env.INFISICAL_URL); +const client = url.protocol === 'https:' ? https : http; + +const post = (path, body) => new Promise((resolve, reject) => { + const data = JSON.stringify(body); + const req = client.request({ hostname: url.hostname, port: url.port, path, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } + }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(JSON.parse(d))); }); + req.on('error', reject); + req.end(data); +}); + +const get = (path, token) => new Promise((resolve, reject) => { + const req = client.request({ hostname: url.hostname, port: url.port, path, method: 'GET', + headers: { 'Authorization': 'Bearer ' + token } + }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(JSON.parse(d))); }); + req.on('error', reject); + req.end(); +}); + +(async () => { + try { + const auth = await post('/api/v1/auth/universal-auth/login', { + clientId: process.env.INFISICAL_CLIENT_ID, + clientSecret: process.env.INFISICAL_CLIENT_SECRET + }); + if (!auth.accessToken) { console.error('[infisical] Auth failed'); process.exit(1); } + + const slug = process.env.INFISICAL_PROJECT_SLUG; + const env = process.env.INFISICAL_ENV; + const secrets = await get('/api/v3/secrets/raw?workspaceSlug=' + slug + '&environment=' + env + '&secretPath=/&recursive=true', auth.accessToken); + + if (!secrets.secrets) { console.error('[infisical] No secrets returned'); process.exit(1); } + + // Output as shell-safe export statements + for (const s of secrets.secrets) { + // Single-quote the value to prevent shell expansion, escape existing single quotes + const escaped = s.secretValue.replace(/'/g, \"'\\\\''\" ); + console.log('export ' + s.secretKey + \"='\" + escaped + \"'\"); + } + } catch (e) { console.error('[infisical] Error:', e.message); process.exit(1); } +})(); +" 2>&1) || { + echo "[infisical] WARNING: Failed to fetch secrets, starting with existing env vars" + exec "$@" +} + +# Check if we got export statements or error messages +if echo "$EXPORTS" | grep -q "^export "; then + COUNT=$(echo "$EXPORTS" | grep -c "^export ") + eval "$EXPORTS" + echo "[infisical] Injected ${COUNT} secrets" +else + echo "[infisical] WARNING: $EXPORTS" + echo "[infisical] Starting with existing env vars" +fi + +exec "$@"