fix: expose coolify production envs

This commit is contained in:
thibaud-leclere 2026-05-06 16:30:10 +02:00
parent b1d70f2603
commit 860000d0b3
5 changed files with 134 additions and 16 deletions

View file

@ -104,6 +104,24 @@ La prod utilise uniquement `docker-compose.prod.yml`.
Ce compose ne contient pas de `build:`. Il tire les images du registry.
Sur Coolify, `docker-compose.prod.yml` est la source de vérité des variables
d'environnement. Les variables sont déclarées dans les blocs `environment:` des
services pour que Coolify les crée dans son UI au chargement du compose. Il ne
faut pas compter sur un fichier `.env` du dépôt en prod : il est ignoré par Git.
Variables générées/préremplies pour Coolify :
- `SERVICE_PASSWORD_POSTGRES` : mot de passe PostgreSQL partagé entre la base et le backend
- `SERVICE_BASE64_DATA_ENCRYPTION_KEY` : clé stable de 32 caractères pour `DATA_ENCRYPTION_KEY`
- `SERVICE_URL_HOPPSCOTCH_APP` : URL publique de l'app
- `SERVICE_URL_HOPPSCOTCH_ADMIN` : URL publique de l'admin
- `SERVICE_URL_HOPPSCOTCH_BACKEND` : URL publique du backend
- `SERVICE_FQDN_HOPPSCOTCH_BACKEND` : domaine du backend, utilisé pour `VITE_BACKEND_WS_URL`
Après l'import du compose dans Coolify, vérifier au minimum que les trois URLs
publiques correspondent aux domaines assignés aux services app, admin et
backend.
Démarrer avec le tag `latest` :
```sh

View file

@ -3,9 +3,9 @@ services:
image: postgres:15
user: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-testpass}
POSTGRES_DB: ${POSTGRES_DB:-hoppscotch}
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES:-testpass}
- POSTGRES_DB=${POSTGRES_DB:-hoppscotch}
volumes:
- hoppscotch-db:/var/lib/postgresql/data
healthcheck:
@ -22,19 +22,26 @@ services:
container_name: hoppscotch-backend
restart: unless-stopped
image: ${API_CLIENT_REGISTRY:-forge.lclr.dev}/${API_CLIENT_NAMESPACE:-thibaud-lclr}/${API_CLIENT_IMAGE_PREFIX:-api-client}-backend:${API_CLIENT_TAG:-latest}
env_file:
- ./.env
environment:
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-testpass}@hoppscotch-db:5432/${POSTGRES_DB:-hoppscotch}
- DATABASE_URL=postgresql://postgres:${SERVICE_PASSWORD_POSTGRES:-testpass}@hoppscotch-db:5432/${POSTGRES_DB:-hoppscotch}
- DATA_ENCRYPTION_KEY=${SERVICE_BASE64_DATA_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef}
- VITE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
- VITE_SHORTCODE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
- VITE_ADMIN_URL=${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
- VITE_BACKEND_GQL_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/graphql
- VITE_BACKEND_WS_URL=wss://${SERVICE_FQDN_HOPPSCOTCH_BACKEND:-localhost:3170}/graphql
- VITE_BACKEND_API_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/v1
- VITE_APP_TOS_LINK=${VITE_APP_TOS_LINK:-https://docs.hoppscotch.io/support/terms}
- VITE_APP_PRIVACY_POLICY_LINK=${VITE_APP_PRIVACY_POLICY_LINK:-https://docs.hoppscotch.io/support/privacy}
- VITE_PROXYSCOTCH_ACCESS_TOKEN=${VITE_PROXYSCOTCH_ACCESS_TOKEN:-}
- ENABLE_SUBPATH_BASED_ACCESS=${ENABLE_SUBPATH_BASED_ACCESS:-false}
- WHITELISTED_ORIGINS=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000},${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
- TRUST_PROXY=${TRUST_PROXY:-true}
depends_on:
hoppscotch-db:
condition: service_healthy
command:
[
"sh",
"-c",
"pnpm exec prisma migrate deploy && node prod_run.mjs",
]
["sh", "-c", "pnpm exec prisma migrate deploy && node prod_run.mjs"]
ports:
- "3170:3170"
@ -42,8 +49,17 @@ services:
container_name: hoppscotch-app
restart: unless-stopped
image: ${API_CLIENT_REGISTRY:-forge.lclr.dev}/${API_CLIENT_NAMESPACE:-thibaud-lclr}/${API_CLIENT_IMAGE_PREFIX:-api-client}-app:${API_CLIENT_TAG:-latest}
env_file:
- ./.env
environment:
- VITE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
- VITE_SHORTCODE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
- VITE_ADMIN_URL=${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
- VITE_BACKEND_GQL_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/graphql
- VITE_BACKEND_WS_URL=wss://${SERVICE_FQDN_HOPPSCOTCH_BACKEND:-localhost:3170}/graphql
- VITE_BACKEND_API_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/v1
- VITE_APP_TOS_LINK=${VITE_APP_TOS_LINK:-https://docs.hoppscotch.io/support/terms}
- VITE_APP_PRIVACY_POLICY_LINK=${VITE_APP_PRIVACY_POLICY_LINK:-https://docs.hoppscotch.io/support/privacy}
- VITE_PROXYSCOTCH_ACCESS_TOKEN=${VITE_PROXYSCOTCH_ACCESS_TOKEN:-}
- ENABLE_SUBPATH_BASED_ACCESS=${ENABLE_SUBPATH_BASED_ACCESS:-false}
depends_on:
hoppscotch-backend:
condition: service_started
@ -55,8 +71,16 @@ services:
container_name: hoppscotch-sh-admin
restart: unless-stopped
image: ${API_CLIENT_REGISTRY:-forge.lclr.dev}/${API_CLIENT_NAMESPACE:-thibaud-lclr}/${API_CLIENT_IMAGE_PREFIX:-api-client}-sh-admin:${API_CLIENT_TAG:-latest}
env_file:
- ./.env
environment:
- VITE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
- VITE_SHORTCODE_BASE_URL=${SERVICE_URL_HOPPSCOTCH_APP:-http://localhost:3000}
- VITE_ADMIN_URL=${SERVICE_URL_HOPPSCOTCH_ADMIN:-http://localhost:3100}
- VITE_BACKEND_GQL_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/graphql
- VITE_BACKEND_API_URL=${SERVICE_URL_HOPPSCOTCH_BACKEND:-http://localhost:3170}/v1
- VITE_APP_TOS_LINK=${VITE_APP_TOS_LINK:-https://docs.hoppscotch.io/support/terms}
- VITE_APP_PRIVACY_POLICY_LINK=${VITE_APP_PRIVACY_POLICY_LINK:-https://docs.hoppscotch.io/support/privacy}
- VITE_PROXYSCOTCH_ACCESS_TOKEN=${VITE_PROXYSCOTCH_ACCESS_TOKEN:-}
- ENABLE_SUBPATH_BASED_ACCESS=${ENABLE_SUBPATH_BASED_ACCESS:-false}
depends_on:
hoppscotch-backend:
condition: service_started

View file

@ -0,0 +1,39 @@
import { ConfigService } from '@nestjs/config';
import { resolveProductionCorsOrigins } from './cors-config';
describe('resolveProductionCorsOrigins', () => {
const configService = (values: Record<string, string | undefined>) =>
({
get: (key: string) => values[key],
}) as ConfigService;
test('Should use WHITELISTED_ORIGINS when it is configured', () => {
expect(
resolveProductionCorsOrigins(
configService({
WHITELISTED_ORIGINS:
'https://app.example.test, https://admin.example.test',
VITE_BASE_URL: 'https://ignored.example.test',
VITE_ADMIN_URL: 'https://ignored-admin.example.test',
}),
),
).toEqual(['https://app.example.test', 'https://admin.example.test']);
});
test('Should fall back to app and admin URLs when WHITELISTED_ORIGINS is missing', () => {
expect(
resolveProductionCorsOrigins(
configService({
VITE_BASE_URL: 'https://app.example.test',
VITE_ADMIN_URL: 'https://admin.example.test',
}),
),
).toEqual(['https://app.example.test', 'https://admin.example.test']);
});
test('Should explain the missing production CORS configuration', () => {
expect(() => resolveProductionCorsOrigins(configService({}))).toThrow(
'Missing production CORS configuration. Set WHITELISTED_ORIGINS or configure VITE_BASE_URL/VITE_ADMIN_URL.',
);
});
});

View file

@ -0,0 +1,36 @@
import { ConfigService } from '@nestjs/config';
const MISSING_PRODUCTION_CORS_CONFIG =
'Missing production CORS configuration. Set WHITELISTED_ORIGINS or configure VITE_BASE_URL/VITE_ADMIN_URL.';
function parseOrigins(origins: string | undefined): string[] {
return (
origins
?.split(',')
.map((origin) => origin.trim())
.filter(Boolean) ?? []
);
}
export function resolveProductionCorsOrigins(
configService: ConfigService,
): string[] {
const whitelistedOrigins = parseOrigins(
configService.get('WHITELISTED_ORIGINS'),
);
if (whitelistedOrigins.length > 0) {
return whitelistedOrigins;
}
const derivedOrigins = [
configService.get('VITE_BASE_URL'),
configService.get('VITE_ADMIN_URL'),
].filter(Boolean);
if (derivedOrigins.length === 0) {
throw new Error(MISSING_PRODUCTION_CORS_CONFIG);
}
return derivedOrigins;
}

View file

@ -9,6 +9,7 @@ import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { InfraTokenModule } from './infra-token/infra-token.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { resolveProductionCorsOrigins } from './cors-config';
function setupSwagger(
app: NestExpressApplication,
@ -58,7 +59,7 @@ async function bootstrap() {
if (isProduction) {
console.log('Enabling CORS with production settings');
app.enableCors({
origin: configService.get('WHITELISTED_ORIGINS').split(','),
origin: resolveProductionCorsOrigins(configService),
credentials: true,
});
} else {