Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s

Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA),
apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/;
infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e
workflow Gitea Actions per il deploy via SSH a ogni push su master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
This commit is contained in:
Lorenzo Sanesi
2026-10-20 09:30:48 +02:00
co-authored by Claude Opus 5.5
commit b83ce039fb
505 changed files with 84544 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
# 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: