Skip to content

Service Configuration

Norm Brandinger edited this page Nov 20, 2025 · 1 revision

Service Profiles - Flexible Container Orchestration

Overview

DevStack Core provides multiple service profiles to accommodate different development scenarios. This allows developers to run only the services they need, reducing resource consumption and startup time.

Available Profiles

1. minimal - Essential Services Only

Use Case: Basic development with Git server and single database

Services: (5 containers, ~2GB RAM)

  • vault - Secrets management (required)
  • postgres - Primary database + Git storage
  • pgbouncer - Connection pooling
  • forgejo - Git server
  • redis-1 - Single Redis instance (non-cluster mode)

Start Command:

./devstack.sh start --profile minimal
# OR
docker compose --profile minimal up -d

Best For:

  • Simple CRUD application development
  • Git repository hosting only
  • Learning the platform
  • Resource-constrained environments

2. standard - Full Development Stack

Use Case: Multi-database development without observability

Services: (12 containers, ~4GB RAM)

  • All minimal services
  • mysql - Legacy database support
  • mongodb - NoSQL document database
  • redis-1, redis-2, redis-3 - Redis cluster (3 nodes)
  • rabbitmq - Message queue

Start Command:

./devstack.sh start --profile standard
# OR
docker compose --profile standard up -d

Best For:

  • Multi-database applications
  • Microservices development
  • Message queue integration
  • Redis cluster testing (your use case!)

3. full - Complete Suite with Observability

Use Case: Production-parity development with full monitoring

Services: (18 containers, ~6GB RAM)

  • All standard services
  • prometheus - Metrics collection
  • grafana - Visualization dashboards
  • loki - Log aggregation
  • vector - Observability pipeline
  • cadvisor - Container monitoring
  • redis-exporter-1/2/3 - Redis metrics exporters

Start Command:

./devstack.sh start --profile full
# OR
docker compose --profile full up -d

Best For:

  • Performance testing and optimization
  • Production troubleshooting simulation
  • Understanding system resource usage
  • Learning observability patterns

4. reference - Include Reference Applications

Use Case: API development and cross-language pattern learning

Additional Services: (5 reference apps)

  • reference-api - Python FastAPI (code-first)
  • api-first - Python FastAPI (API-first)
  • golang-api - Go with Gin
  • nodejs-api - Node.js with Express
  • rust-api - Rust with Actix-web (partial)

Start Command:

./devstack.sh start --profile standard --profile reference
# OR
docker compose --profile standard --profile reference up -d

Best For:

  • Learning API design patterns
  • Comparing language implementations
  • Testing shared test suites
  • API integration examples

Profile Comparison

Profile Containers RAM Usage Startup Time Use Case
minimal 5 ~2GB 20s Git + Basic DB
standard 12 ~4GB 45s Full dev stack
full 18 ~6GB 60s With observability
reference +5 +1GB +15s Add-on for APIs

Implementation Approach

Recommended: Docker Compose Profiles + Python Management Script

Why this approach:

  1. Docker Compose Native Profiles - Built-in feature since Compose v1.28+

    • No custom orchestration logic needed
    • Well-documented and widely understood
    • Native support for --profile flag
    • Services can belong to multiple profiles
  2. Python Management Script - Replace bash with Python

    • Better error handling and validation
    • Easier to test and maintain
    • Rich library ecosystem (click, rich, pyyaml)
    • Cross-platform compatibility (future Linux support)
    • Type hints for better IDE support
  3. Profile Configuration File - YAML-based profile definitions

    • Centralized profile management
    • Easy to add custom profiles
    • Documented service groupings
    • User-extensible

Architecture

devstack-core/
├── devstack.py              # New Python management script
├── profiles.yaml                   # Profile definitions
├── docker-compose.yml              # Updated with profile labels
└── configs/
    └── profiles/
        ├── minimal.yaml            # Minimal profile env overrides
        ├── standard.yaml           # Standard profile env overrides
        └── full.yaml               # Full profile env overrides

Docker Compose Profile Implementation

Step 1: Update docker-compose.yml

Add profiles: key to each service:

services:
  # ALWAYS STARTED (no profile = default)
  vault:
    image: vault:latest
    # No profiles key - starts in all profiles

  # MINIMAL PROFILE
  postgres:
    profiles: ["minimal", "standard", "full"]
    image: postgres:18

  forgejo:
    profiles: ["minimal", "standard", "full"]
    image: forgejo:1.21

  redis-1:
    profiles: ["minimal", "standard", "full"]
    image: redis:7.4-alpine3.21
    # In minimal mode, runs standalone (no cluster init)

  # STANDARD PROFILE (adds these)
  redis-2:
    profiles: ["standard", "full"]  # NOT in minimal
    image: redis:7.4-alpine3.21

  redis-3:
    profiles: ["standard", "full"]  # NOT in minimal
    image: redis:7.4-alpine3.21

  mysql:
    profiles: ["standard", "full"]
    image: mysql:8.0

  mongodb:
    profiles: ["standard", "full"]
    image: mongo:7

  rabbitmq:
    profiles: ["standard", "full"]
    image: rabbitmq:3-management-alpine

  # FULL PROFILE (adds observability)
  prometheus:
    profiles: ["full"]
    image: prom/prometheus:v2.48.0

  grafana:
    profiles: ["full"]
    image: grafana/grafana:10.2.2

  loki:
    profiles: ["full"]
    image: grafana/loki:2.9.3

  # REFERENCE APPS (separate profile, combinable)
  reference-api:
    profiles: ["reference"]
    build: ./reference-apps/fastapi

Step 2: profiles.yaml Configuration

profiles:
  minimal:
    description: "Essential services only (Git + single DB)"
    services:
      - vault
      - postgres
      - pgbouncer
      - forgejo
      - redis-1
    ram_estimate: "2GB"
    env_overrides:
      REDIS_CLUSTER_ENABLED: "false"  # Single node mode

  standard:
    description: "Full development stack (multi-DB + Redis cluster)"
    services:
      - vault
      - postgres
      - pgbouncer
      - mysql
      - mongodb
      - redis-1
      - redis-2
      - redis-3
      - rabbitmq
      - forgejo
    ram_estimate: "4GB"
    env_overrides:
      REDIS_CLUSTER_ENABLED: "true"  # Enable cluster

  full:
    description: "Complete suite with observability"
    extends: standard
    additional_services:
      - prometheus
      - grafana
      - loki
      - vector
      - cadvisor
      - redis-exporter-1
      - redis-exporter-2
      - redis-exporter-3
    ram_estimate: "6GB"

  reference:
    description: "Reference API applications"
    services:
      - reference-api
      - api-first
      - golang-api
      - nodejs-api
      - rust-api
    ram_estimate: "+1GB"
    combinable: true  # Can combine with other profiles

# Custom user profiles (optional)
custom_profiles:
  redis-dev:
    description: "Redis cluster development only"
    services:
      - vault
      - redis-1
      - redis-2
      - redis-3
    ram_estimate: "1.5GB"

  postgres-dev:
    description: "PostgreSQL development only"
    services:
      - vault
      - postgres
      - pgbouncer
    ram_estimate: "1GB"

Step 3: Python Management Script (devstack.py)

#!/usr/bin/env python3
"""
DevStack Core Management Script
Python-based orchestration with profile support
"""
import click
import subprocess
import yaml
from pathlib import Path
from rich.console import Console
from rich.table import Table
from typing import List, Dict, Optional

console = Console()

# Load profiles configuration
PROFILES_FILE = Path(__file__).parent / "profiles.yaml"
COMPOSE_FILE = Path(__file__).parent / "docker-compose.yml"

def load_profiles() -> Dict:
    """Load profile definitions from YAML"""
    with open(PROFILES_FILE) as f:
        return yaml.safe_load(f)

def get_profile_services(profile_name: str) -> List[str]:
    """Get list of services for a profile"""
    profiles = load_profiles()
    if profile_name in profiles['profiles']:
        return profiles['profiles'][profile_name]['services']
    elif profile_name in profiles.get('custom_profiles', {}):
        return profiles['custom_profiles'][profile_name]['services']
    else:
        raise ValueError(f"Unknown profile: {profile_name}")

@click.group()
def cli():
    """DevStack Core - Flexible Development Infrastructure"""
    pass

@cli.command()
@click.option('--profile', '-p', multiple=True, default=['standard'],
              help='Service profile(s) to start (minimal/standard/full/reference)')
@click.option('--detach/--no-detach', '-d', default=True,
              help='Run in background (detached mode)')
def start(profile: tuple, detach: bool):
    """Start Colima VM and Docker services with specified profile(s)"""

    # Validate profiles
    profiles_config = load_profiles()
    for p in profile:
        if p not in profiles_config['profiles'] and \
           p not in profiles_config.get('custom_profiles', {}):
            console.print(f"[red]Error: Unknown profile '{p}'[/red]")
            return

    # Display what will start
    console.print(f"\n[cyan]Starting DevStack Core with profile(s): {', '.join(profile)}[/cyan]\n")

    # Start Colima if not running
    result = subprocess.run(['colima', 'status'],
                          capture_output=True, text=True)
    if result.returncode != 0:
        console.print("[yellow]Starting Colima VM...[/yellow]")
        subprocess.run(['colima', 'start',
                       '--cpu', '4', '--memory', '8', '--disk', '60',
                       '--network-address'])

    # Build docker compose command with profiles
    cmd = ['docker', 'compose']
    for p in profile:
        cmd.extend(['--profile', p])
    cmd.extend(['up', '-d' if detach else ''])

    # Execute
    console.print(f"[green]Executing: {' '.join(cmd)}[/green]")
    subprocess.run(cmd)

    # Show what started
    console.print("\n[green]✓ Services started successfully[/green]")
    subprocess.run(['docker', 'compose', 'ps'])

@cli.command()
@click.option('--profile', '-p', multiple=True,
              help='Only stop services from specific profile(s)')
def stop(profile: Optional[tuple]):
    """Stop Docker services and Colima VM"""
    if profile:
        # Stop specific profile services
        cmd = ['docker', 'compose']
        for p in profile:
            cmd.extend(['--profile', p])
        cmd.append('down')
        subprocess.run(cmd)
    else:
        # Stop everything
        subprocess.run(['docker', 'compose', 'down'])
        subprocess.run(['colima', 'stop'])

@cli.command()
def profiles():
    """List available service profiles"""
    profiles_config = load_profiles()

    table = Table(title="Available Service Profiles")
    table.add_column("Profile", style="cyan")
    table.add_column("Services", style="green")
    table.add_column("RAM", style="yellow")
    table.add_column("Description")

    for name, config in profiles_config['profiles'].items():
        services = str(len(config['services']))
        table.add_row(
            name,
            services,
            config.get('ram_estimate', 'N/A'),
            config['description']
        )

    console.print(table)

    # Show custom profiles if any
    if 'custom_profiles' in profiles_config:
        console.print("\n[cyan]Custom Profiles:[/cyan]")
        for name, config in profiles_config['custom_profiles'].items():
            console.print(f"  • {name}: {config['description']}")

@cli.command()
@click.argument('service', required=False)
def logs(service: Optional[str]):
    """View logs for all services or specific service"""
    if service:
        subprocess.run(['docker', 'compose', 'logs', '-f', service])
    else:
        subprocess.run(['docker', 'compose', 'logs', '-f'])

@cli.command()
def status():
    """Show status of all running services"""
    subprocess.run(['docker', 'compose', 'ps'])

@cli.command()
@click.argument('service')
def shell(service: str):
    """Open shell in a running container"""
    subprocess.run(['docker', 'compose', 'exec', service, 'sh'])

# ... more commands (health, backup, vault-*, etc.)

if __name__ == '__main__':
    cli()

Migration Plan

Phase 1: Add Profile Support to docker-compose.yml (Week 1)

  1. Add profile labels to all services

    • Categorize services into minimal/standard/full
    • Test each profile independently
    • Document service dependencies
  2. Create profiles.yaml configuration

    • Define profile hierarchies
    • Set RAM estimates
    • Add environment overrides
  3. Update documentation

    • Add SERVICE_PROFILES.md (this document)
    • Update INSTALLATION.md with profile examples
    • Add to README.md quick start

Phase 2: Create Python Management Script (Week 2)

  1. Implement core functionality

    • start/stop/restart with profile support
    • profile listing and inspection
    • status and health checks
  2. Add advanced features

    • Vault operations
    • Backup/restore
    • Forgejo initialization
  3. Parallel operation

    • Keep devstack.sh working
    • Users can choose which to use
    • Eventually deprecate bash version

Phase 3: Testing and Refinement (Week 3)

  1. Test all profile combinations

    • minimal alone
    • standard alone
    • full alone
    • standard + reference
    • custom profiles
  2. Performance validation

    • Measure actual RAM usage
    • Verify startup times
    • Test on different Mac specs
  3. Documentation polish

    • Add troubleshooting section
    • Create video demonstrations
    • Update all docs to reference profiles

Usage Examples

Developer Scenarios

Scenario 1: Backend Developer (PostgreSQL Only)

# Start minimal services
./devstack.py start --profile minimal

# Work with PostgreSQL
psql -h localhost -p 5432 -U dev_admin -d dev_database

# Stop when done
./devstack.py stop

Scenario 2: Redis Cluster Developer (Your Use Case)

# Start standard profile (includes Redis cluster)
./devstack.py start --profile standard

# Initialize Redis cluster
docker exec dev-redis-1 redis-cli --cluster create \
  172.20.0.13:6379 172.20.0.16:6379 172.20.0.17:6379 \
  --cluster-yes

# Test cluster operations
redis-cli -c -h localhost -p 6379 cluster nodes

Scenario 3: Full-Stack Developer with Observability

# Start everything
./devstack.py start --profile full

# Access Grafana dashboards
open http://localhost:3001

# View metrics in Prometheus
open http://localhost:9090

Scenario 4: API Developer Learning Patterns

# Start standard + reference apps
./devstack.py start --profile standard --profile reference

# Compare implementations
curl http://localhost:8000/health  # Python FastAPI
curl http://localhost:8002/health  # Go Gin
curl http://localhost:8003/health  # Node.js Express

Environment Variable Overrides

Each profile has specific environment variable overrides in configs/profiles/. These files customize service behavior based on the profile's use case and resource constraints.

Profile Configuration Files

  • configs/profiles/minimal.env - Essential services (5 containers, ~2GB RAM)
  • configs/profiles/standard.env - Full dev stack (12 containers, ~4GB RAM)
  • configs/profiles/full.env - Complete with observability (18 containers, ~6GB RAM)
  • configs/profiles/reference.env - Reference APIs addon (~+1GB RAM)

Redis Configuration Comparison

Variable Minimal Standard Full Why Different Impact
REDIS_CLUSTER_ENABLED false true true Minimal uses single Redis instance Cluster provides HA & sharding
REDIS_CLUSTER_INIT_REQUIRED false true true No cluster init needed for single node Standard/full require cluster setup
REDIS_MAX_MEMORY 128mb 256mb 256mb Lower memory for single instance Per-node allocation
REDIS_HEALTH_INTERVAL 30s 60s 60s Faster checks for quicker startup Standard intervals for production-like

PostgreSQL Configuration Comparison

Variable Minimal Standard Full Why Different Impact
POSTGRES_MAX_CONNECTIONS 50 100 100 Fewer services in minimal Standard supports more clients
POSTGRES_SHARED_BUFFERS 128MB 256MB 256MB Reduced memory allocation Smaller buffer cache
POSTGRES_EFFECTIVE_CACHE_SIZE 512MB 1GB 1GB Conservative estimate Query planner optimization
POSTGRES_WORK_MEM 4MB 8MB 8MB Smaller per-operation memory Sort/hash operation capacity
POSTGRES_ENABLE_TLS false false false Local dev simplicity TLS optional for all
POSTGRES_HEALTH_INTERVAL 30s 60s 60s Faster startup feedback Standard production-like
POSTGRES_MEMORY_LIMIT 1G 2G 2G Docker resource constraint Container memory cap
POSTGRES_MEMORY_RESERVATION 256M 512M 512M Guaranteed minimum Reserved memory
POSTGRES_CPU_LIMIT 1.5 2 2 CPU allocation Max CPU cores

Resource Limits Comparison

Service Minimal Memory Standard Memory Full Memory Why Different
PostgreSQL 1G limit / 256M reserved 2G limit / 512M reserved 2G limit / 512M reserved More connections/workload
MySQL Not included 1G limit / 256M reserved 1G limit / 256M reserved Only in standard+
MongoDB Not included 1G limit / 256M reserved 1G limit / 256M reserved Only in standard+
Redis (per node) 256M limit / 128M reserved 512M limit / 256M reserved 512M limit / 256M reserved Cluster needs more memory
RabbitMQ Not included 512M limit / 256M reserved 512M limit / 256M reserved Only in standard+
Vault 256M limit / 128M reserved 512M limit / 128M reserved 512M limit / 128M reserved Minimal secrets workload
Forgejo 512M limit / 256M reserved 1G limit / 256M reserved 1G limit / 256M reserved Git server needs more
Prometheus Not included Not included 1G limit / 512M reserved Only in full
Grafana Not included Not included 512M limit / 256M reserved Only in full
Loki Not included Not included 512M limit / 256M reserved Only in full

Health Check Configuration Comparison

Service Minimal Interval Standard Interval Full Interval Why Different Impact
Vault 30s 60s 60s Faster startup validation More frequent checks
PostgreSQL 30s 60s 60s Quick feedback loop Faster detection
Redis 30s 60s 60s Lightweight checks Minimal overhead
Forgejo 30s 60s 60s Faster ready state Quick startup
MySQL N/A 60s 60s Only in standard+ Standard interval
MongoDB N/A 60s 60s Only in standard+ Standard interval
RabbitMQ N/A 60s 60s Only in standard+ Standard interval
Prometheus N/A N/A 30s Only in full, lighter Monitoring service
Grafana N/A N/A 30s Only in full, lighter UI service

Logging Configuration Comparison

Variable Minimal Standard Full Why Different Impact
LOG_LEVEL info info info Consistent logging Balanced verbosity
DOCKER_LOG_MAX_SIZE 5m 10m 20m Log retention Disk space usage
DOCKER_LOG_MAX_FILE 2 3 5 File rotation Total log history
LOG_FORMAT Not set Not set json Structured for observability Loki integration
LOG_TIMESTAMPS Not set Not set true Timestamp correlation Log analysis

Feature Flags Comparison

Variable Minimal Standard Full Why Different Impact
ENABLE_METRICS false false true No Prometheus in minimal/standard Metrics collection
ENABLE_LOGS false false true No Loki in minimal/standard Log aggregation
ENABLE_TLS false false false Simplified for dev Local only
ENABLE_OBSERVABILITY false false true No Vector in minimal/standard Pipeline processing
ENABLE_CONTAINER_MONITORING false false true No cAdvisor in minimal/standard Container metrics
ENABLE_REDIS_EXPORTERS false false true No exporters in minimal/standard Redis metrics

PgBouncer Configuration Comparison

Variable Minimal Standard Full Why Different Impact
PGBOUNCER_DEFAULT_POOL_SIZE 5 10 10 Fewer connections needed Pool capacity
PGBOUNCER_MAX_CLIENT_CONN 25 100 100 Limited client count Connection limit
PGBOUNCER_MAX_DB_CONNECTIONS 10 20 20 Total backend connections PostgreSQL load

Backup Configuration Comparison

Variable Minimal Standard Full Why Different Impact
BACKUP_RETENTION_DAYS 7 14 30 Storage optimization Disk usage
BACKUP_SCHEDULE 0 2 * * 0 (weekly) 0 2 * * * (daily) 0 2 * * * (daily) Less frequent for minimal Backup frequency
BACKUP_COMPRESSION true true true Save disk space File size
BACKUP_POSTGRES true true true All profiles Data safety
BACKUP_MYSQL false true true Not in minimal Service presence
BACKUP_MONGODB false true true Not in minimal Service presence
BACKUP_PROMETHEUS false false true Only full has metrics Metrics retention
BACKUP_GRAFANA false false true Only full has dashboards Dashboard config

Observability Configuration (Full Profile Only)

Variable Value Purpose Impact
PROMETHEUS_SCRAPE_INTERVAL 15s Metrics collection frequency Data granularity
PROMETHEUS_EVALUATION_INTERVAL 15s Alert evaluation frequency Alert latency
PROMETHEUS_RETENTION_TIME 15d Metrics storage duration Disk usage
PROMETHEUS_RETENTION_SIZE 10GB Max storage size Disk space limit
LOKI_RETENTION_PERIOD 744h (31 days) Log storage duration Disk usage
LOKI_CHUNK_TARGET_SIZE 1572864 Chunk compression Storage efficiency
VECTOR_LOG_LEVEL info Pipeline logging Debug verbosity
CADVISOR_HOUSEKEEPING_INTERVAL 30s Container stats collection CPU overhead
REDIS_EXPORTER_SCRAPE_INTERVAL 15s Redis metrics frequency Network overhead

Reference Profile Configuration (Addon)

Variable Value Purpose Impact
ENABLE_REFERENCE_APPS true Enable APIs Service startup
REFERENCE_API_LOG_LEVEL info Application logging Log volume
REFERENCE_API_ENABLE_METRICS true Prometheus metrics Metrics endpoints
REFERENCE_API_ENABLE_TLS true HTTPS endpoints Certificate usage
REFERENCE_API_STRUCTURED_LOGGING true JSON logging Log parsing
FASTAPI_WORKERS 4 Uvicorn workers CPU usage
GOLANG_API_LOG_LEVEL info Go service logging Log verbosity
NODEJS_API_CLUSTER_ENABLED false Node.js clustering Process count
RUST_API_WORKERS 4 Actix workers Thread pool

Network Configuration (All Profiles)

Variable Value Purpose Impact
COLIMA_NETWORK_ADDRESS true Enable host access External connectivity
SERVICE_NETWORK_SUBNET 172.20.0.0/16 Network range IP allocation

Security Settings (All Profiles - Development Mode)

Variable Value Purpose Why This Value
ALLOW_WEAK_PASSWORDS true Local development Convenience over security
ENABLE_RATE_LIMITING false Local development Testing without limits
SESSION_TIMEOUT 86400 (minimal), 28800 (standard/full) Session duration Development convenience
ENABLE_TLS false TLS/SSL Local development simplicity

Complete Variable Summary by Profile

Minimal Profile (configs/profiles/minimal.env):

  • 298 lines of configuration
  • Focus: Resource efficiency, quick startup
  • Key differences: Single Redis, reduced limits, shorter retention
  • Target: Personal projects, learning, CI/CD

Standard Profile (configs/profiles/standard.env):

  • 460 lines of configuration
  • Focus: Production parity, multi-database
  • Key differences: Redis cluster, all databases, standard limits
  • Target: Multi-service development, integration testing

Full Profile (configs/profiles/full.env):

  • 486 lines of configuration
  • Focus: Complete observability, monitoring
  • Key differences: All observability stack, enhanced logging, longer retention
  • Target: Performance analysis, troubleshooting, learning observability

Reference Profile (configs/profiles/reference.env):

  • 455 lines of configuration
  • Focus: API examples, cross-language patterns
  • Combinable: Must be used with minimal, standard, or full
  • Target: API development, pattern learning, performance comparison

Advanced: Custom Profiles

Users can define their own profiles in profiles.yaml:

custom_profiles:
  my-microservices:
    description: "My custom microservices stack"
    services:
      - vault
      - postgres
      - redis-1
      - redis-2
      - redis-3
      - rabbitmq
      - prometheus
    ram_estimate: "3GB"
    env_file: configs/profiles/my-microservices.env

Then use it:

./devstack.py start --profile my-microservices

Benefits

  1. Resource Efficiency

    • Run only what you need
    • Minimal: 2GB vs Full: 6GB (3x savings)
    • Faster startup times
  2. Developer Experience

    • Clear service groupings
    • Easy to understand what runs
    • Quick profile switching
  3. Maintainability

    • Docker Compose native feature
    • Python for complex logic
    • YAML for configuration
    • Easy to extend
  4. Flexibility

    • Combine multiple profiles
    • Create custom profiles
    • Override environment per profile
  5. Documentation

    • Self-documenting profiles
    • Clear use case descriptions
    • Built-in help system

Recommendation Summary

✅ RECOMMENDED APPROACH:

  1. Use Docker Compose native profiles - Add profiles: key to services
  2. Migrate to Python management script - Replace 1600-line bash with maintainable Python
  3. YAML-based profile configuration - Centralized, documented, extensible
  4. Three core profiles - minimal/standard/full with reference as add-on
  5. Parallel operation during migration - Keep bash script until Python is stable

Why NOT alternatives:

  • Makefile - Not suitable for complex logic, poor error handling
  • Multiple docker-compose files - Harder to maintain, more complex
  • Pure bash with flags - Already have 1600 lines, would become unmaintainable
  • Custom orchestration - Reinventing Docker Compose features

Timeline: 3 weeks to implement, test, and document

Effort: ~40 hours of work (spread across 3 weeks)

Impact: Significant improvement in developer experience and resource efficiency

Clone this wiki locally