Skip to main content

Feature 3: Configuration Cleanup

Purpose: Clear dev/prod separation with consistent naming, runtime configuration for frontend.

Priority: HIGH (production readiness) Estimated Time: 4-5 hours Dependencies: None


Overview​

This feature establishes clear separation between development and production configurations.

Naming Convention (consistent across frontend and backend):

TypeDev (default)Prod (explicit)
Frontendenvironment.tsenvironment.prod.ts
Backendapplication.ymlapplication-prod.yml
Dockerdocker-compose.ymldocker-compose.yml (same file)

What This Fixes:

  1. Hardcoded Stripe keys in frontend (can't change without rebuild)
  2. No production Spring profile
  3. No Docker production configuration
  4. Inconsistent dev/prod separation

Current Wiring Analysis​

How Each File Gets Used​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ FRONTEND β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ng serve │────▢│ angular.json │────▢│ defaultConfig: β”‚ β”‚
β”‚ β”‚ (local dev) β”‚ β”‚ serve.default β”‚ β”‚ "development" β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ Uses: environment.ts β”‚
β”‚ apiUrl: 'http://localhost:8081'β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ng build │────▢│ angular.json │────▢│ defaultConfig: β”‚ β”‚
β”‚ β”‚ (Dockerfile)β”‚ β”‚ build.default β”‚ β”‚ "production" β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ Uses: environment.prod.ts β”‚
β”‚ apiUrl: '' (nginx proxies) β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ BACKEND β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ gradlew │────▢│ No profile set │────▢│ Loads: β”‚ β”‚
β”‚ β”‚ bootRun β”‚ β”‚ (or dev profile)β”‚ β”‚ application.yml β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + application-dev.yml β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Dockerfile │────▢│ No profile set │────▢│ Loads: β”‚ β”‚
β”‚ β”‚ java -jar β”‚ β”‚ (ISSUE! ⚠️) β”‚ β”‚ application.yml only β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ (has dev settings!) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DOCKER COMPOSE β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ docker-compose.yml (all environments): β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ springular-client: β”‚ β”‚
β”‚ β”‚ ports: "80:4200" βœ… Standard HTTP port β”‚ β”‚
β”‚ β”‚ STRIPE_PUBLIC_KEY: ${...} βœ… Runtime injection β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ springular-server: β”‚ β”‚
β”‚ β”‚ SPRING_PROFILES_ACTIVE: prod βœ… Loads application-prod.yml β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ springular-docs: β”‚ β”‚
β”‚ β”‚ ports: "3000:3000" βœ… Docs service β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Current Issues​

ComponentIssueImpact
application.ymlHas devtools enabledDevtools in Docker builds
docker-compose.ymlNo SPRING_PROFILES_ACTIVEBackend uses dev settings
client/DockerfileNo entrypointCan't inject runtime config
environment.prod.tsHardcoded Stripe keyRequires rebuild to change

Trigger Summary: When is Each File Used?​

TriggerFrontend FileBackend File
ng serveenvironment.tsN/A
ng buildenvironment.prod.tsN/A
./gradlew bootRunN/Aapplication.yml
./gradlew bootRun + SPRING_PROFILES_ACTIVE=devN/Aapplication.yml + application-dev.yml
./gradlew bootRun + SPRING_PROFILES_ACTIVE=prodN/Aapplication.yml + application-prod.yml
docker-compose upenvironment.prod.ts + config.jsonapplication.yml + application-prod.yml

Architecture​

Backend Configuration Strategy​

FilePurposeWhen Used
application.ymlDev defaultsAlways loaded (base config)
application-prod.ymlProd overridesSPRING_PROFILES_ACTIVE=prod

Frontend Configuration Strategy​

FilePurposeWhen Used
environment.tsDev defaultsng serve / ng build
environment.prod.tsProd build-timeng build --configuration production
/assets/config.jsonRuntime configLoaded at app startup in Docker

Frontend Runtime Config Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Docker Start β”‚
β”‚ (entrypoint.sh) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ generates
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ /assets/config.jsonβ”‚
β”‚ from ENV vars β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ loaded by
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ConfigService β”‚
β”‚ (APP_INITIALIZER) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ provides
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Angular App β”‚
β”‚ (uses config) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 1: Create Frontend Runtime Config Service​

Problem: Stripe public key hardcoded - requires rebuild to change.

1.1 Create Config Interface​

File: client/src/app/core/config/app-config.interface.ts (NEW)

export interface AppConfig {
stripe: {
publicKey: string;
};
}

1.2 Create Config Service​

File: client/src/app/core/config/config.service.ts (NEW)

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { firstValueFrom } from "rxjs";
import { environment } from "../../../environments/environment";
import { AppConfig } from "./app-config.interface";

@Injectable({
providedIn: "root",
})
export class ConfigService {
private config: AppConfig | null = null;

constructor(private http: HttpClient) {}

async loadConfig(): Promise<void> {
if (environment.production) {
// Production: load from runtime config file
try {
this.config = await firstValueFrom(
this.http.get<AppConfig>("/assets/config.json")
);
} catch (error) {
console.error("Failed to load config.json, using defaults", error);
this.config = this.getDefaults();
}
} else {
// Development: use environment file directly
this.config = {
stripe: {
publicKey: environment.stripe.publicKey,
},
};
}
}

get stripePublicKey(): string {
return this.config?.stripe?.publicKey || environment.stripe.publicKey;
}

private getDefaults(): AppConfig {
return {
stripe: {
publicKey: environment.stripe.publicKey,
},
};
}
}

1.3 Register APP_INITIALIZER​

File: client/src/main.ts (UPDATE)

Note: This project uses standalone bootstrap (not NgModule), so we update main.ts instead of app.config.ts.

import { APP_INITIALIZER, enableProdMode, importProvidersFrom } from "@angular/core";
import { ConfigService } from "./app/core/config";

// Factory function for config initialization
function initializeApp(configService: ConfigService): () => Promise<void> {
return () => configService.loadConfig();
}

bootstrapApplication(AppComponent, {
providers: [
// ... existing providers ...
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [ConfigService],
multi: true,
},
],
});

1.4 Update Components to Use ConfigService​

File: Any component using Stripe public key

// Before:
import { environment } from "../environments/environment";
const stripeKey = environment.stripe.publicKey;

// After:
import { ConfigService } from "./core/config/config.service";

constructor(private configService: ConfigService) {}

ngOnInit() {
const stripeKey = this.configService.stripePublicKey;
}

Step 2: Create Docker Entrypoint for Runtime Config​

Problem: Need to generate config.json from environment variables at container startup.

2.1 Create Entrypoint Script​

File: client/docker-entrypoint.sh (NEW)

#!/bin/sh
set -e

# Generate runtime config from environment variables
cat > /usr/share/nginx/html/assets/config.json <<EOF
{
"stripe": {
"publicKey": "${STRIPE_PUBLIC_KEY:-pk_test_default}"
}
}
EOF

echo "Generated /assets/config.json with runtime configuration"

# Start nginx
exec nginx -g "daemon off;"

2.2 Update Dockerfile​

File: client/Dockerfile (UPDATE)

# ----------------------------
# build from source
# ----------------------------
FROM node:18 AS build

WORKDIR /app

COPY package*.json .
RUN npm install

COPY . .
RUN npm run build

# ----------------------------
# run with nginx
# ----------------------------
FROM nginx:1.27

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
COPY --from=build /app/dist/springular /usr/share/nginx/html

# Add entrypoint script
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

EXPOSE 4200

# Use entrypoint instead of default nginx command
ENTRYPOINT ["/docker-entrypoint.sh"]

Step 3: Create Production Spring Profile​

Problem: No explicit production configuration.

3.1 Create application-prod.yml​

File: server/src/main/resources/application-prod.yml (NEW)

# Production profile overrides
# Activated with: SPRING_PROFILES_ACTIVE=prod

spring:
devtools:
restart:
enabled: false
livereload:
enabled: false
jpa:
hibernate:
ddl-auto: validate # Never auto-update schema in prod
mvc:
log-request-details: false

logging:
level:
root: WARN
com.saas.springular: INFO
org.springframework.security: WARN
org.springframework.web: WARN
org.hibernate.type.descriptor.sql.BasicBinder: OFF

3.2 Keep application.yml as Dev Defaults​

File: server/src/main/resources/application.yml

No changes needed - current config with devtools enabled IS the dev default.

3.3 Simplify application-dev.yml​

File: server/src/main/resources/application-dev.yml

Can be deleted or kept minimal (base config already has dev settings).


Step 4: Create Docker Compose Configuration​

Problem: No production-ready Docker configuration that works with Coolify's proxy.

4.1 Create Production Config (docker-compose.yml)​

File: docker-compose.yml (in project root)

Key Strategy: No port mappings - Coolify's Traefik proxy handles all routing

# Docker Compose Configuration
# All services use Coolify's Traefik proxy (no direct port exposure)
#
# Coolify Setup:
# Build Pack: Docker Compose
# Domain Assignment (in Coolify dashboard):
# - springular-client: https://yourdomain.com (port 4200)
# - springular-docs: https://docs.yourdomain.com (port 3000)
# - springular-server: https://api.yourdomain.com (port 8081, optional)

services:
springular-db:
image: "postgres:15.1"
container_name: springular-db
restart: unless-stopped
env_file:
- .env
# No ports - internal network only
networks:
- app-network
volumes:
- springular-db-data:/var/lib/postgresql/data

springular-client:
container_name: springular-client
restart: always
env_file:
- .env
build: ./client
# No ports - Coolify routes via domain to internal port 4200
networks:
- app-network

springular-server:
container_name: springular-server
restart: on-failure
env_file:
- .env
environment:
SPRING_APPLICATION_JSON: '{
"spring.datasource.url": "jdbc:postgresql://springular-db:5432/${POSTGRES_DB:-springular}"
}'
SPRING_PROFILES_ACTIVE: prod
build:
context: ./server
depends_on:
- springular-db
# No ports - Coolify routes via domain to internal port 8081
networks:
- app-network

springular-docs:
container_name: springular-docs
restart: on-failure
build: ./docs
# No ports - Coolify routes via domain to internal port 3000

networks:
app-network:
driver: bridge

volumes:
springular-db-data:

4.2 Create Local Dev Override (docker-compose.override.yml)​

File: docker-compose.override.yml (in project root)

Purpose: Automatically adds port mappings for local development

# Docker Compose Local Development Overrides
# Automatically loaded when running `docker-compose up` locally
# Coolify ignores this file

services:
springular-db:
ports:
- "5432:5432"

springular-client:
ports:
- "4200:4200"

springular-server:
ports:
- "8081:8081"
environment:
# Override to dev profile for local development
SPRING_PROFILES_ACTIVE: dev

springular-docs:
ports:
- "3000:3000"

4.3 Coolify Deployment Configuration​

In Coolify dashboard:

  1. Create New Resource β†’ Select repository
  2. Build Pack: Docker Compose
  3. Docker Compose Location: docker-compose.yml (default)
  4. Set environment variables in Coolify UI (they override .env)

Domain Assignment (after deployment):

ServiceDomainInternal Port
springular-clienthttps://yourdomain.com4200
springular-docshttps://docs.yourdomain.com3000
springular-serverhttps://api.yourdomain.com8081 (optional)

Benefits:

  • βœ… Automatic SSL/HTTPS via Let's Encrypt
  • βœ… No port conflicts (Traefik handles routing)
  • βœ… Clean URLs (no port numbers)
  • βœ… Better security (no direct port access)

4.4 Usage​

Local Development:

docker-compose up
# Automatically merges base + override
# Access: http://localhost:4200, :8081, :3000

Coolify Production:

# Coolify only reads docker-compose.yml
# Access via domains assigned in dashboard

Step 5: Update Logback for Profile-Based Logging​

File: server/src/main/resources/logback-spring.xml (UPDATE)

<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<!-- Development (default): verbose logging -->
<springProfile name="!prod">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.saas.springular" level="DEBUG"/>
<logger name="org.springframework.web" level="DEBUG"/>
</springProfile>

<!-- Production: minimal logging -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.saas.springular" level="INFO"/>
<logger name="org.springframework.security" level="WARN"/>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="OFF"/>
</springProfile>
</configuration>

Summary​

Files to Create​

FilePurpose
client/src/app/core/config/app-config.interface.tsConfig type definition
client/src/app/core/config/config.service.tsRuntime config loader
client/docker-entrypoint.shGenerates config.json from env vars
server/src/main/resources/application-prod.ymlProduction Spring profile
docker-compose.ymlProduction config (no port mappings for Coolify)
docker-compose.override.ymlLocal dev config (adds port mappings)

Files to Update​

FileChange
client/src/main.tsAdd APP_INITIALIZER for ConfigService (standalone bootstrap)
client/DockerfileUse entrypoint script
server/src/main/resources/logback-spring.xmlProfile-based logging

Files to Delete (Optional)​

FileReason
application-dev.ymlNo longer needed (base is dev)

Configuration Matrix​

SettingDev (local)Prod (Coolify)
Docker Composedocker-compose.yml + docker-compose.override.ymldocker-compose.yml only
Spring Profiledevprod
DevToolsβœ… Enabled❌ Disabled
Request loggingβœ… Enabled❌ Disabled
Log levelDEBUGINFO/WARN
DDL autoupdatevalidate
Stripe key sourceenvironment.ts/assets/config.json
Port mappingsβœ… Yes (via override)❌ No (Traefik proxy)
Accesslocalhost:4200, :8081, :3000https://yourdomain.com
SSL❌ Noβœ… Yes (Let's Encrypt)

Environment Variables​

Dev (local docker-compose):

# docker-compose.override.yml sets:
SPRING_PROFILES_ACTIVE=dev # Uses application-dev.yml
# Stripe uses hardcoded test key from environment.ts

Prod (Coolify):

# docker-compose.yml sets:
SPRING_PROFILES_ACTIVE=prod # Uses application-prod.yml
STRIPE_PUBLIC_KEY=pk_live_xxxxx # Runtime injection via Coolify dashboard
# ... other prod env vars

Usage​

Development (Local):

# Option 1: Native development (no Docker)
# Terminal 1: Backend
./gradlew bootRun

# Terminal 2: Frontend
ng serve

# Option 2: Docker development (with ports)
docker-compose up
# Automatically merges docker-compose.yml + docker-compose.override.yml
# Access: http://localhost:4200, :8081, :3000

Production (Coolify):

# Coolify automatically:
# 1. Reads docker-compose.yml only (ignores override)
# 2. Builds and deploys all services
# 3. Routes via Traefik proxy to assigned domains

# No manual docker commands needed - handled by Coolify

Validation Checklist​

Local Development:

  • Dev: Stripe uses key from environment.ts
  • Dev: DevTools active, verbose logs (SPRING_PROFILES_ACTIVE=dev)
  • Docker: Access via localhost:4200, :8081, :3000
  • All ports accessible without domain configuration

Coolify Production:

  • Prod: Stripe uses key from config.json (generated from env var)
  • Prod: DevTools inactive, minimal logs (SPRING_PROFILES_ACTIVE=prod)
  • Docker: No port mappings (services only accessible via domains)
  • Domains assigned in Coolify dashboard
  • SSL/HTTPS working (Let's Encrypt)
  • Can change Stripe key without rebuild (just restart container)

Quick Reference: When to Use What​

Local Development (No Docker)​

# Terminal 1: Backend
./gradlew bootRun
# Uses: application.yml (dev settings already there)

# Terminal 2: Frontend
ng serve
# Uses: environment.ts
# Calls: http://localhost:8081/api directly

Docker Local Development​

docker-compose up
# Automatically merges: docker-compose.yml + docker-compose.override.yml
# Client: environment.prod.ts + /assets/config.json (runtime Stripe key)
# Server: application.yml + application-dev.yml (SPRING_PROFILES_ACTIVE=dev)
# Access: http://localhost:4200, :8081, :3000 (all ports exposed)

Docker Production (Coolify)​

# Coolify reads: docker-compose.yml only (no override)
# Client: environment.prod.ts + /assets/config.json (runtime Stripe key)
# Server: application.yml + application-prod.yml (SPRING_PROFILES_ACTIVE=prod)
# Access: Via domains assigned in Coolify (no port mappings)
# SSL: Automatic via Let's Encrypt

File Purpose Summary​

FileContainsUsed By
environment.tsDev config (apiUrl to localhost:8081)ng serve
environment.prod.tsProd config (apiUrl empty for nginx)ng build / Docker
/assets/config.jsonRuntime config (Stripe key)Docker at startup
application.ymlBase configAlways loaded first
application-dev.ymlDev overrides (verbose logging)SPRING_PROFILES_ACTIVE=dev
application-prod.ymlProd overrides (no devtools, minimal logs)SPRING_PROFILES_ACTIVE=prod
docker-compose.ymlProduction config (no ports, prod profile)Coolify (and base for local)
docker-compose.override.ymlLocal dev overrides (adds ports, dev profile)Local development only