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):
| Type | Dev (default) | Prod (explicit) |
|---|---|---|
| Frontend | environment.ts | environment.prod.ts |
| Backend | application.yml | application-prod.yml |
| Docker | docker-compose.yml | docker-compose.yml (same file) |
What This Fixes:
- Hardcoded Stripe keys in frontend (can't change without rebuild)
- No production Spring profile
- No Docker production configuration
- 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β
| Component | Issue | Impact |
|---|---|---|
application.yml | Has devtools enabled | Devtools in Docker builds |
docker-compose.yml | No SPRING_PROFILES_ACTIVE | Backend uses dev settings |
client/Dockerfile | No entrypoint | Can't inject runtime config |
environment.prod.ts | Hardcoded Stripe key | Requires rebuild to change |
Trigger Summary: When is Each File Used?β
| Trigger | Frontend File | Backend File |
|---|---|---|
ng serve | environment.ts | N/A |
ng build | environment.prod.ts | N/A |
./gradlew bootRun | N/A | application.yml |
./gradlew bootRun + SPRING_PROFILES_ACTIVE=dev | N/A | application.yml + application-dev.yml |
./gradlew bootRun + SPRING_PROFILES_ACTIVE=prod | N/A | application.yml + application-prod.yml |
docker-compose up | environment.prod.ts + config.json | application.yml + application-prod.yml |
Architectureβ
Backend Configuration Strategyβ
| File | Purpose | When Used |
|---|---|---|
application.yml | Dev defaults | Always loaded (base config) |
application-prod.yml | Prod overrides | SPRING_PROFILES_ACTIVE=prod |
Frontend Configuration Strategyβ
| File | Purpose | When Used |
|---|---|---|
environment.ts | Dev defaults | ng serve / ng build |
environment.prod.ts | Prod build-time | ng build --configuration production |
/assets/config.json | Runtime config | Loaded 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:
- Create New Resource β Select repository
- Build Pack: Docker Compose
- Docker Compose Location:
docker-compose.yml(default) - Set environment variables in Coolify UI (they override
.env)
Domain Assignment (after deployment):
| Service | Domain | Internal Port |
|---|---|---|
| springular-client | https://yourdomain.com | 4200 |
| springular-docs | https://docs.yourdomain.com | 3000 |
| springular-server | https://api.yourdomain.com | 8081 (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β
| File | Purpose |
|---|---|
client/src/app/core/config/app-config.interface.ts | Config type definition |
client/src/app/core/config/config.service.ts | Runtime config loader |
client/docker-entrypoint.sh | Generates config.json from env vars |
server/src/main/resources/application-prod.yml | Production Spring profile |
docker-compose.yml | Production config (no port mappings for Coolify) |
docker-compose.override.yml | Local dev config (adds port mappings) |
Files to Updateβ
| File | Change |
|---|---|
client/src/main.ts | Add APP_INITIALIZER for ConfigService (standalone bootstrap) |
client/Dockerfile | Use entrypoint script |
server/src/main/resources/logback-spring.xml | Profile-based logging |
Files to Delete (Optional)β
| File | Reason |
|---|---|
application-dev.yml | No longer needed (base is dev) |
Configuration Matrixβ
| Setting | Dev (local) | Prod (Coolify) |
|---|---|---|
| Docker Compose | docker-compose.yml + docker-compose.override.yml | docker-compose.yml only |
| Spring Profile | dev | prod |
| DevTools | β Enabled | β Disabled |
| Request logging | β Enabled | β Disabled |
| Log level | DEBUG | INFO/WARN |
| DDL auto | update | validate |
| Stripe key source | environment.ts | /assets/config.json |
| Port mappings | β Yes (via override) | β No (Traefik proxy) |
| Access | localhost:4200, :8081, :3000 | https://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β
| File | Contains | Used By |
|---|---|---|
environment.ts | Dev config (apiUrl to localhost:8081) | ng serve |
environment.prod.ts | Prod config (apiUrl empty for nginx) | ng build / Docker |
/assets/config.json | Runtime config (Stripe key) | Docker at startup |
application.yml | Base config | Always loaded first |
application-dev.yml | Dev overrides (verbose logging) | SPRING_PROFILES_ACTIVE=dev |
application-prod.yml | Prod overrides (no devtools, minimal logs) | SPRING_PROFILES_ACTIVE=prod |
docker-compose.yml | Production config (no ports, prod profile) | Coolify (and base for local) |
docker-compose.override.yml | Local dev overrides (adds ports, dev profile) | Local development only |