# Produzione ITI TECHNOLOGY (VPS). Variabili da `.env` nella root (modello: .env.prod.example). # npm run deploy:build → build delle immagini # npm run deploy:up → avvio/aggiornamento, attende i healthcheck # Solo Caddy espone porte (80/443). Procedura completa (primo avvio, seed, backup, DNS) in CLAUDE.md → "Deploy". name: iti-technology x-logging: &logging driver: json-file options: max-size: "10m" max-file: "5" x-db-env: &db-env PGHOST: postgres PGUSER: ${POSTGRES_USER:?POSTGRES_USER mancante in .env} PGPASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD mancante in .env} APP_DB_NAME: ${APP_DB_NAME:?APP_DB_NAME mancante in .env} APP_DB_USER: ${APP_DB_USER:?APP_DB_USER mancante in .env} MEDUSA_DB_NAME: ${MEDUSA_DB_NAME:?MEDUSA_DB_NAME mancante in .env} MEDUSA_DB_USER: ${MEDUSA_DB_USER:?MEDUSA_DB_USER mancante in .env} services: postgres: build: ./infra/postgres image: iti-technology/postgres:16-3.4-pgvector restart: unless-stopped logging: *logging environment: POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER mancante in .env} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD mancante in .env} POSTGRES_DB: ${POSTGRES_DB:-postgres} APP_DB_NAME: ${APP_DB_NAME:?APP_DB_NAME mancante in .env} APP_DB_USER: ${APP_DB_USER:?APP_DB_USER mancante in .env} APP_DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD mancante in .env} MEDUSA_DB_NAME: ${MEDUSA_DB_NAME:?MEDUSA_DB_NAME mancante in .env} MEDUSA_DB_USER: ${MEDUSA_DB_USER:?MEDUSA_DB_USER mancante in .env} MEDUSA_DB_PASSWORD: ${MEDUSA_DB_PASSWORD:?MEDUSA_DB_PASSWORD mancante in .env} volumes: - postgres-data:/var/lib/postgresql/data - ./infra/postgres/init:/docker-entrypoint-initdb.d:ro networks: [backend] healthcheck: test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] interval: 5s timeout: 5s retries: 30 start_period: 30s redis: image: redis:7-alpine restart: unless-stopped logging: *logging command: ["redis-server", "--appendonly", "yes"] volumes: - redis-data:/data networks: [backend] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 20 medusa: build: ./medusa image: iti-technology/medusa:latest restart: unless-stopped logging: *logging depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } environment: NODE_ENV: production PORT: "9000" # sslmode=disable: con NODE_ENV=production Medusa usa SSL verso Postgres; qui è sulla rete interna DATABASE_URL: postgres://${MEDUSA_DB_USER}:${MEDUSA_DB_PASSWORD}@postgres:5432/${MEDUSA_DB_NAME}?sslmode=disable MEDUSA_DISABLE_TELEMETRY: "true" REDIS_URL: redis://redis:6379 # Il sito non chiama Medusa (passa dall'API); il dashboard è su shop-admin STORE_CORS: https://${DOMAIN:?DOMAIN mancante in .env} ADMIN_CORS: https://shop-admin.${DOMAIN} AUTH_CORS: https://shop-admin.${DOMAIN} JWT_SECRET: ${MEDUSA_JWT_SECRET:?MEDUSA_JWT_SECRET mancante in .env} COOKIE_SECRET: ${MEDUSA_COOKIE_SECRET:?MEDUSA_COOKIE_SECRET mancante in .env} AUTH_MFA_ENCRYPTION_KEY: ${MEDUSA_AUTH_MFA_ENCRYPTION_KEY:?MEDUSA_AUTH_MFA_ENCRYPTION_KEY mancante in .env} FILE_UPLOAD_DIR: /data/medusa-files FILE_BACKEND_URL: https://media.${DOMAIN}/shop STRIPE_API_KEY: ${STRIPE_API_KEY:-} STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-} INTERNAL_API_SECRET: ${INTERNAL_API_SECRET:?INTERNAL_API_SECRET mancante in .env} FASTIFY_WEBHOOK_URL: http://api:3000 WEBHOOK_SECRET: ${WEBHOOK_SECRET:?WEBHOOK_SECRET mancante in .env} volumes: - medusa-files:/data/medusa-files # backend: Postgres e Redis; edge: Caddy, API e uscita verso Stripe networks: [backend, edge] api: build: context: . dockerfile: apps/api/Dockerfile image: iti-technology/api:latest restart: unless-stopped logging: *logging depends_on: postgres: { condition: service_healthy } medusa: { condition: service_started } environment: NODE_ENV: production HOST: 0.0.0.0 PORT: "3000" LOG_LEVEL: ${API_LOG_LEVEL:-info} LOG_DIR: /var/log/iti # Fidati solo dei proxy sulla rete Docker (Caddy e server SSR): un client diretto non può falsificare X-Forwarded-For TRUST_PROXY: "loopback,uniquelocal" DATABASE_URL: postgres://${APP_DB_USER}:${APP_DB_PASSWORD}@postgres:5432/${APP_DB_NAME} CORS_ORIGINS: https://${DOMAIN},https://area.${DOMAIN} COOKIE_SECRET: ${API_COOKIE_SECRET:?API_COOKIE_SECRET mancante in .env} JWT_SECRET: ${API_JWT_SECRET:?API_JWT_SECRET mancante in .env} # Cookie di sessione condivisi tra sito e area riservata AUTH_COOKIE_DOMAIN: ${DOMAIN} AUTH_COOKIE_SECURE: "true" PRIVATE_APP_URL: https://area.${DOMAIN} PRIVACY_POLICY_VERSION: ${PRIVACY_POLICY_VERSION:-privacy-v1} ADMIN_NOTIFY_EMAILS: ${ADMIN_NOTIFY_EMAILS:-} UPLOAD_DIR: /data/uploads UPLOAD_MAX_BYTES: ${UPLOAD_MAX_BYTES:-26214400} PUBLIC_UPLOADS_URL: https://media.${DOMAIN} EMAIL_PROVIDER: ${EMAIL_PROVIDER:-console} EMAIL_FROM: ${EMAIL_FROM:-} EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-ITI TECHNOLOGY} EMAIL_REPLY_TO: ${EMAIL_REPLY_TO:-} BREVO_API_KEY: ${BREVO_API_KEY:-} RESEND_API_KEY: ${RESEND_API_KEY:-} MEDUSA_URL: http://medusa:9000 MEDUSA_PAYMENT_PROVIDER_ID: pp_stripe_stripe # Stampata da seed-base.ts al primo avvio (npm run deploy:seed:medusa): finché manca l'API non parte MEDUSA_PUBLISHABLE_KEY: ${MEDUSA_PUBLISHABLE_KEY:-} INTERNAL_API_SECRET: ${INTERNAL_API_SECRET} WEBHOOK_SECRET: ${WEBHOOK_SECRET} # Solo per `npm run db:seed:prod` (admin iniziale) SEED_ADMIN_EMAIL: ${SEED_ADMIN_EMAIL:-} SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD:-} volumes: - uploads:/data/uploads - api-logs:/var/log/iti networks: [backend, edge] public: build: context: . dockerfile: apps/public/Dockerfile image: iti-technology/public:latest restart: unless-stopped logging: *logging depends_on: api: { condition: service_started } environment: NODE_ENV: production PORT: "4000" SITE_URL: https://${DOMAIN} PUBLIC_API_URL: https://api.${DOMAIN} API_INTERNAL_URL: http://api:3000 # Immagini di blog e prodotti: img-src della CSP che il server SSR manda con il nonce PUBLIC_MEDIA_URL: https://media.${DOMAIN} PRIVATE_APP_URL: https://area.${DOMAIN} # true solo sul dominio definitivo: altrimenti noindex ovunque e robots.txt "Disallow: /" SITE_INDEXABLE: ${SITE_INDEXABLE:-false} STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY:-} NG_ALLOWED_HOSTS: ${DOMAIN} networks: [edge] caddy: build: context: . dockerfile: infra/caddy/Dockerfile image: iti-technology/caddy:latest restart: unless-stopped logging: *logging depends_on: # Il volume uploads viene popolato (public/, private/) dall'immagine dell'API quando il suo container # viene creato: serve al mount con subpath qui sotto api: { condition: service_started } public: { condition: service_started } medusa: { condition: service_started } ports: - "80:80" - "443:443" - "443:443/udp" environment: DOMAIN: ${DOMAIN} ACME_EMAIL: ${ACME_EMAIL:?ACME_EMAIL mancante in .env} SHOP_ADMIN_USER: ${SHOP_ADMIN_USER:?SHOP_ADMIN_USER mancante in .env} SHOP_ADMIN_PASSWORD_HASH: ${SHOP_ADMIN_PASSWORD_HASH:?SHOP_ADMIN_PASSWORD_HASH mancante in .env} SHOP_ADMIN_ALLOWED_IPS: ${SHOP_ADMIN_ALLOWED_IPS:-0.0.0.0/0 ::/0} volumes: # Solo uploads/public: uploads/private non è nemmeno visibile al container - type: volume source: uploads target: /srv/uploads read_only: true volume: subpath: public - medusa-files:/srv/medusa-files:ro - caddy-data:/data - caddy-config:/config # Siti aggiuntivi facoltativi (*.caddy) - ./infra/caddy/sites:/etc/caddy/sites:ro networks: [edge] # --- Strumenti (profilo "tools": non partono con `up`) --- backup: image: iti-technology/postgres:16-3.4-pgvector profiles: [tools] depends_on: postgres: { condition: service_healthy } environment: <<: *db-env BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-14} entrypoint: ["sh", "/scripts/backup.sh"] volumes: - ./infra/backup:/scripts:ro - ${BACKUP_DIR:-./backups}:/backups - uploads:/data/uploads:ro - medusa-files:/data/medusa-files:ro networks: [backend] restore: image: iti-technology/postgres:16-3.4-pgvector profiles: [tools] depends_on: postgres: { condition: service_healthy } environment: <<: *db-env entrypoint: ["sh", "/scripts/restore.sh"] volumes: - ./infra/backup:/scripts:ro - ${BACKUP_DIR:-./backups}:/backups:ro - uploads:/data/uploads - medusa-files:/data/medusa-files networks: [backend] networks: # Postgres e Redis: nessuna uscita verso Internet, raggiungibili solo da Medusa, API e strumenti backend: internal: true edge: volumes: postgres-data: redis-data: uploads: medusa-files: api-logs: caddy-data: caddy-config: