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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-23 19:25:47 -08:00
parent 44357ba0a7
commit ce8463951c
3 changed files with 93 additions and 6 deletions

View File

@ -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"]

View File

@ -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

82
entrypoint.sh Normal file
View File

@ -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 "$@"