#!/bin/bash # Run this from /opt/stacks/vorteq on expvtcasp01 # Creates all infrastructure files for the Vorteq Quest Portal set -e echo "Creating infrastructure files..." # ============================================================ # docker-compose.yml # ============================================================ cat > docker-compose.yml << 'DCOMPOSE' version: '3.8' services: # Traefik Reverse Proxy traefik: image: traefik:v3.0 container_name: traefik restart: unless-stopped security_opt: - no-new-privileges:true networks: - web ports: - "80:80" - "443:443" - "8080:8080" environment: - CF_DNS_API_TOKEN=${CLOUDFLARE_DNS_API_TOKEN} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik/traefik.yml:/traefik.yml:ro - ./traefik/acme.json:/acme.json - ./traefik/config:/config:ro labels: - "traefik.enable=true" - "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN}`)" - "traefik.http.routers.traefik.entrypoints=websecure" - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" - "traefik.http.routers.traefik.service=api@internal" - "traefik.http.routers.traefik.middlewares=auth" - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$8evjzm8w$$FU3G.qPxZRGlnUKzPJ6QB/" # PostgreSQL postgres: image: postgres:16-alpine container_name: postgres restart: unless-stopped networks: - web environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_MULTIPLE_DATABASES: vorteq_dev,vorteq_test,vorteq_prod volumes: - postgres_data:/var/lib/postgresql/data - ./init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 # Redis redis: image: redis:7-alpine container_name: redis restart: unless-stopped networks: - web volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 # Development Environment app-dev: image: forgejo.wulfconsulting.cloud/lorentz/quest-vorteq:dev container_name: vorteq-dev restart: unless-stopped networks: - web environment: NODE_ENV: development DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/vorteq_dev REDIS_URL: redis://redis:6379/0 BETTER_AUTH_SECRET: ${DEV_AUTH_SECRET} BETTER_AUTH_URL: https://${DEV_DOMAIN} PORT: 3000 labels: - "traefik.enable=true" - "traefik.http.routers.app-dev.rule=Host(`${DEV_DOMAIN}`)" - "traefik.http.routers.app-dev.entrypoints=websecure" - "traefik.http.routers.app-dev.tls.certresolver=letsencrypt" - "traefik.http.services.app-dev.loadbalancer.server.port=3000" - "traefik.http.routers.app-dev.middlewares=dev-headers" - "traefik.http.middlewares.dev-headers.headers.customresponseheaders.X-Environment=development" depends_on: postgres: condition: service_healthy redis: condition: service_healthy # Testing Environment app-test: image: forgejo.wulfconsulting.cloud/lorentz/quest-vorteq:test container_name: vorteq-test restart: unless-stopped networks: - web environment: NODE_ENV: test DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/vorteq_test REDIS_URL: redis://redis:6379/1 BETTER_AUTH_SECRET: ${TEST_AUTH_SECRET} BETTER_AUTH_URL: https://${TEST_DOMAIN} PORT: 3000 labels: - "traefik.enable=true" - "traefik.http.routers.app-test.rule=Host(`${TEST_DOMAIN}`)" - "traefik.http.routers.app-test.entrypoints=websecure" - "traefik.http.routers.app-test.tls.certresolver=letsencrypt" - "traefik.http.services.app-test.loadbalancer.server.port=3000" - "traefik.http.routers.app-test.middlewares=test-headers" - "traefik.http.middlewares.test-headers.headers.customresponseheaders.X-Environment=testing" depends_on: postgres: condition: service_healthy redis: condition: service_healthy # Production Environment app-prod: image: forgejo.wulfconsulting.cloud/lorentz/quest-vorteq:latest container_name: vorteq-prod restart: unless-stopped networks: - web environment: NODE_ENV: production DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/vorteq_prod REDIS_URL: redis://redis:6379/2 BETTER_AUTH_SECRET: ${PROD_AUTH_SECRET} BETTER_AUTH_URL: https://${PROD_DOMAIN} PORT: 3000 labels: - "traefik.enable=true" - "traefik.http.routers.app-prod.rule=Host(`${PROD_DOMAIN}`)" - "traefik.http.routers.app-prod.entrypoints=websecure" - "traefik.http.routers.app-prod.tls.certresolver=letsencrypt" - "traefik.http.services.app-prod.loadbalancer.server.port=3000" - "traefik.http.routers.app-prod.middlewares=prod-chain" - "traefik.http.middlewares.prod-chain.chain.middlewares=security-headers,rate-limit,compress" - "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000" - "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true" - "traefik.http.middlewares.security-headers.headers.stsPreload=true" - "traefik.http.middlewares.security-headers.headers.forceSTSHeader=true" - "traefik.http.middlewares.security-headers.headers.frameDeny=true" - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true" - "traefik.http.middlewares.security-headers.headers.browserXssFilter=true" - "traefik.http.middlewares.rate-limit.ratelimit.average=100" - "traefik.http.middlewares.rate-limit.ratelimit.burst=50" - "traefik.http.middlewares.compress.compress=true" depends_on: postgres: condition: service_healthy redis: condition: service_healthy deploy: resources: limits: cpus: '2' memory: 2G reservations: cpus: '1' memory: 1G networks: web: external: true volumes: postgres_data: redis_data: DCOMPOSE echo " docker-compose.yml" # ============================================================ # traefik/traefik.yml # ============================================================ mkdir -p traefik/{config,logs} cat > traefik/traefik.yml << 'TRAEFIK' api: dashboard: true insecure: false entryPoints: web: address: ":80" http: redirections: entryPoint: to: websecure scheme: https websecure: address: ":443" providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false network: web file: directory: /config watch: true certificatesResolvers: letsencrypt: acme: email: lorentz@wulfconsulting.com storage: acme.json dnsChallenge: provider: cloudflare delayBeforeCheck: 0 resolvers: - "1.1.1.1:53" - "8.8.8.8:53" log: level: INFO accessLog: filePath: /var/log/traefik/access.log bufferingSize: 100 TRAEFIK echo " traefik/traefik.yml" # ============================================================ # init-databases.sh # ============================================================ cat > init-databases.sh << 'INITDB' #!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE DATABASE vorteq_dev; GRANT ALL PRIVILEGES ON DATABASE vorteq_dev TO postgres; CREATE DATABASE vorteq_test; GRANT ALL PRIVILEGES ON DATABASE vorteq_test TO postgres; CREATE DATABASE vorteq_prod; GRANT ALL PRIVILEGES ON DATABASE vorteq_prod TO postgres; EOSQL echo "Multiple databases created successfully!" INITDB chmod +x init-databases.sh echo " init-databases.sh" # ============================================================ # .env.example # ============================================================ cat > .env.example << 'ENVEX' # PostgreSQL POSTGRES_PASSWORD=change-me-to-a-strong-password # Cloudflare DNS Challenge CLOUDFLARE_DNS_API_TOKEN=your-cloudflare-api-token-here ACME_EMAIL=lorentz@wulfconsulting.com # Domains DEV_DOMAIN=dev.quest.vorteq.wulf.cloud TEST_DOMAIN=testing.vorteq.wulf.cloud PROD_DOMAIN=quest.vorteq.wulf.cloud TRAEFIK_DOMAIN=traefik.vorteq.wulf.cloud # Better-Auth Secrets (generate with: openssl rand -base64 32) DEV_AUTH_SECRET=your-dev-secret-here TEST_AUTH_SECRET=your-test-secret-here PROD_AUTH_SECRET=your-prod-secret-here ENVEX echo " .env.example" # ============================================================ # setup.sh # ============================================================ cat > setup.sh << 'SETUP' #!/bin/bash set -e echo "Setting up Traefik Multi-Environment Stack" echo "==============================================" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' echo "Checking prerequisites..." command -v docker >/dev/null 2>&1 || { echo -e "${RED}Docker is not installed${NC}" exit 1 } command -v docker compose >/dev/null 2>&1 || { echo -e "${RED}Docker Compose is not installed${NC}" exit 1 } echo -e "${GREEN}Docker and Docker Compose are installed${NC}" echo "Creating directory structure..." mkdir -p traefik/{config,logs} echo "Setting up SSL certificate storage..." touch traefik/acme.json chmod 600 traefik/acme.json chmod +x init-databases.sh echo "Creating Docker network..." docker network create web 2>/dev/null || echo -e "${YELLOW}Network 'web' already exists${NC}" if [ ! -f .env ]; then echo "Creating .env file..." cp .env.example .env echo -e "${YELLOW}Please edit .env file with your actual values!${NC}" else echo -e "${GREEN}.env file already exists${NC}" fi SERVER_IP=$(curl -s ifconfig.me 2>/dev/null || echo "YOUR_SERVER_IP") echo "" echo "Cloudflare DNS Configuration Check" echo "====================================" echo "Make sure these DNS records are configured in Cloudflare:" echo " - quest.vorteq.wulf.cloud -> ${SERVER_IP}" echo " - dev.quest.vorteq.wulf.cloud -> ${SERVER_IP}" echo " - testing.vorteq.wulf.cloud -> ${SERVER_IP}" echo " - traefik.vorteq.wulf.cloud -> ${SERVER_IP}" echo "" echo "Important: Set DNS records to 'DNS only' (grey cloud)" echo "" # Generate auth secrets if needed if grep -q "your-.*-secret-here" .env 2>/dev/null; then echo -e "${YELLOW}Generating Better-Auth secrets...${NC}" DEV_SECRET=$(openssl rand -base64 32) TEST_SECRET=$(openssl rand -base64 32) PROD_SECRET=$(openssl rand -base64 32) sed -i "s/your-dev-secret-here/$DEV_SECRET/" .env sed -i "s/your-test-secret-here/$TEST_SECRET/" .env sed -i "s/your-prod-secret-here/$PROD_SECRET/" .env echo -e "${GREEN}Generated and saved auth secrets${NC}" else echo -e "${GREEN}Auth secrets already configured${NC}" fi echo "" echo "Pre-flight Checklist" echo "======================" echo "Before running docker compose up, ensure:" echo " - DNS records are configured" echo " - Firewall allows ports 80 and 443" echo " - .env file is configured" echo " - Docker images are built (or registry is accessible)" echo "" read -p "Do you want to start the stack now? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Starting services..." docker compose up -d echo "" echo -e "${GREEN}Stack is starting!${NC}" echo "" echo "View logs with: docker compose logs -f" echo "SSL certificates may take a few minutes to provision" else echo "" echo "Setup complete! Run 'docker compose up -d' when ready." fi SETUP chmod +x setup.sh echo " setup.sh" # ============================================================ # Done # ============================================================ echo "" echo "All files created. Directory listing:" find . -not -path './.git/*' -not -path './.git' | sort echo "" echo "Next steps:" echo " 1. cp .env.example .env" echo " 2. nano .env (fill in your secrets)" echo " 3. git add . && git commit -m 'Initial infrastructure setup' && git push -u origin main"