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:
parent
44357ba0a7
commit
ce8463951c
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
Loading…
Reference in New Issue