Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s
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:
@@ -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:
|
||||
Reference in New Issue
Block a user