Compare commits

..

2 commits

Author SHA1 Message Date
thibaud-leclere
860000d0b3 fix: expose coolify production envs 2026-05-06 16:30:10 +02:00
thibaud-leclere
b1d70f2603 ci: remove registry image workflow 2026-05-06 15:23:23 +02:00
6 changed files with 134 additions and 59 deletions

View file

@ -1,43 +0,0 @@
name: Publish Docker image
on:
push:
tags:
- "*"
env:
REGISTRY: forge.lclr.dev
IMAGE_NAME: thibaud-lclr/api-client
jobs:
publish:
runs-on: docker
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Docker CLI
run: |
apt-get update
apt-get install -y docker.io
- name: Log in to Forgejo registry
run: |
printf '%s' "${{ secrets.FORGEJO_REGISTRY_TOKEN }}" \
| docker login "${REGISTRY}" \
--username "${{ secrets.FORGEJO_REGISTRY_USERNAME }}" \
--password-stdin
- name: Build and push image
run: |
IMAGE_TAG="${REGISTRY}/${IMAGE_NAME}:${{ github.ref_name }}"
docker build \
--file prod.Dockerfile \
--target aio \
--label "org.opencontainers.image.source=https://${REGISTRY}/${IMAGE_NAME}" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.version=${{ github.ref_name }}" \
--tag "${IMAGE_TAG}" \
.
docker push "${IMAGE_TAG}"

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 {