Migrate secrets to Infisical injection at container startup
Replace hardcoded secret env vars in docker-compose with Infisical entrypoint pattern. Secrets (Mollie, Google, SMTP, Listmonk) are now fetched from Infisical API on container start. Also set LISTMONK_LIST_ID default to 27 (new CoFi Registrations list). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a3cdb9c372
commit
6fa2958c75
|
|
@ -36,6 +36,9 @@ 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 entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
@ -43,4 +46,5 @@ EXPOSE 3000
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,22 @@ services:
|
||||||
container_name: cofi-register
|
container_name: cofi-register
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
# Infisical secret injection (secrets fetched at container startup)
|
||||||
|
- INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID}
|
||||||
|
- INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET}
|
||||||
|
- INFISICAL_PROJECT_SLUG=cofi
|
||||||
|
# Non-secret config (defaults inline)
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- MOLLIE_API_KEY=${MOLLIE_API_KEY}
|
|
||||||
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-https://register.collaborative-finance.net}
|
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-https://register.collaborative-finance.net}
|
||||||
- GOOGLE_SERVICE_ACCOUNT_KEY=${GOOGLE_SERVICE_ACCOUNT_KEY}
|
|
||||||
- GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID}
|
|
||||||
- GOOGLE_SHEET_NAME=${GOOGLE_SHEET_NAME:-Registrations}
|
- GOOGLE_SHEET_NAME=${GOOGLE_SHEET_NAME:-Registrations}
|
||||||
|
- BOOKING_SHEET_NAME=${BOOKING_SHEET_NAME:-Sheet1}
|
||||||
- SMTP_HOST=${SMTP_HOST:-mail.rmail.online}
|
- SMTP_HOST=${SMTP_HOST:-mail.rmail.online}
|
||||||
- SMTP_PORT=${SMTP_PORT:-587}
|
- SMTP_PORT=${SMTP_PORT:-587}
|
||||||
- SMTP_USER=${SMTP_USER}
|
|
||||||
- SMTP_PASS=${SMTP_PASS}
|
|
||||||
- EMAIL_FROM=${EMAIL_FROM}
|
|
||||||
- INTERNAL_NOTIFY_EMAIL=${INTERNAL_NOTIFY_EMAIL}
|
|
||||||
- LISTMONK_DB_HOST=${LISTMONK_DB_HOST:-listmonk-db}
|
- LISTMONK_DB_HOST=${LISTMONK_DB_HOST:-listmonk-db}
|
||||||
- LISTMONK_DB_PORT=${LISTMONK_DB_PORT:-5432}
|
- LISTMONK_DB_PORT=${LISTMONK_DB_PORT:-5432}
|
||||||
- LISTMONK_DB_NAME=${LISTMONK_DB_NAME:-listmonk}
|
- LISTMONK_DB_NAME=${LISTMONK_DB_NAME:-listmonk}
|
||||||
- LISTMONK_DB_USER=${LISTMONK_DB_USER:-listmonk}
|
- LISTMONK_DB_USER=${LISTMONK_DB_USER:-listmonk}
|
||||||
- LISTMONK_DB_PASS=${LISTMONK_DB_PASS}
|
- LISTMONK_LIST_ID=${LISTMONK_LIST_ID:-27}
|
||||||
- LISTMONK_LIST_ID=${LISTMONK_LIST_ID:-1}
|
|
||||||
- BOOKING_SHEET_ID=${BOOKING_SHEET_ID}
|
|
||||||
- BOOKING_SHEET_NAME=${BOOKING_SHEET_NAME:-Sheet1}
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.cofi-register.rule=Host(`register.collaborative-finance.net`) || Host(`cofi-register-staging.jeffemmett.com`)"
|
- "traefik.http.routers.cofi-register.rule=Host(`register.collaborative-finance.net`) || Host(`cofi-register-staging.jeffemmett.com`)"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Infisical secret injection entrypoint (Node.js)
|
||||||
|
# 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, INFISICAL_ENV (default: prod),
|
||||||
|
# INFISICAL_URL (default: http://infisical:8080)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export INFISICAL_URL="${INFISICAL_URL:-http://infisical:8080}"
|
||||||
|
export INFISICAL_ENV="${INFISICAL_ENV:-prod}"
|
||||||
|
# IMPORTANT: Set INFISICAL_PROJECT_SLUG in your docker-compose.yml
|
||||||
|
export INFISICAL_PROJECT_SLUG="${INFISICAL_PROJECT_SLUG:?INFISICAL_PROJECT_SLUG must be set}"
|
||||||
|
|
||||||
|
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