# No `version:` key — it is obsolete in Compose v2 and only prints a warning on # every command, which made the delivery checklist's output noisy. services: db: image: docker.arvancloud.ir/postgres:15-alpine # No container_name. Docker container names are global on the host, so a # fixed name (goldbank_db) makes a second Dokploy app of this stack fail # with "container name already in use". Compose names each container from # the project name, which Dokploy sets uniquely per application. environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: ${POSTGRES_DB:-goldbank} # Host port only. The apps use hostname "db" inside the compose network. # Two copies on one machine must publish different host ports; set # EXTERNAL_DB_PORT on the second Dokploy app (5432 stays the local default). ports: - "${EXTERNAL_DB_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-goldbank}"] interval: 5s timeout: 5s retries: 12 api: build: context: ./apps/api dockerfile: Dockerfile # Prisma connects on module init, so a not-yet-ready DB crash-loops the API. depends_on: db: condition: service_healthy environment: - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@db:5432/goldbank?schema=public} - PORT=${PORT:-3000} # Auth. REQUIRED: the API refuses to start without both, each a random # value of 32+ characters, and the two must differ (SEC-04, REL-03). # See docs/SERVER-SETUP-CHECKLIST.md. - JWT_SECRET=${JWT_SECRET:-} - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-} - JWT_CUSTOMER_SECRET=${JWT_CUSTOMER_SECRET:-} - JWT_CUSTOMER_EXPIRES_IN=${JWT_CUSTOMER_EXPIRES_IN:-} # SEC-04: comma-separated browser origins allowed to call the API # (e.g. https://adm.example.ir,https://app.example.ir). REQUIRED: the API # refuses to start with it blank. - CORS_ORIGINS=${CORS_ORIGINS:-} # SEC-05: demo seed data is never created in production unless "true". - SEED_DEMO_DATA=${SEED_DEMO_DATA:-} # ⚠ TEST SERVERS ONLY — a fixed OTP that signs in ANY active customer. # Leave empty in production: the API refuses to start with it set unless # TEST_SERVER_ALLOW_MASTER_CODE=yes, which only a test server may carry # (REL-01). Must be exactly 4 digits to be typable in the apps. - OTP_MASTER_CODE=${OTP_MASTER_CODE:-} - TEST_SERVER_ALLOW_MASTER_CODE=${TEST_SERVER_ALLOW_MASTER_CODE:-} # SMS. All blank = the "relay" gateway with its built-in defaults (D26). # A failed SMS never blocks the boot or an operation (D20). - SMS_PROVIDER=${SMS_PROVIDER:-} - SMS_RELAY_URL=${SMS_RELAY_URL:-} - SMS_RELAY_SECRET=${SMS_RELAY_SECRET:-} - SMS_RELAY_HOST=${SMS_RELAY_HOST:-} - SMS_TREZ_URL=${SMS_TREZ_URL:-} - SMS_TREZ_USERNAME=${SMS_TREZ_USERNAME:-} - SMS_TREZ_PASSWORD=${SMS_TREZ_PASSWORD:-} - SMS_TREZ_SENDER_LINE=${SMS_TREZ_SENDER_LINE:-} # Accounting integration (D27): encrypts the token stored from the admin # panel. Blank falls back to a JWT_SECRET-derived key with a warning. - ACCOUNTING_SECRET_KEY=${ACCOUNTING_SECRET_KEY:-} - ACCOUNTING_WORKER=${ACCOUNTING_WORKER:-} # Admin mobiles told when a customer completes their profile (D22). # Blank = nobody is notified; the upload still succeeds. - PROFILE_ADMIN_MOBILES=${PROFILE_ADMIN_MOBILES:-} # Central IAM. Read at process start from this container only. # Frontends compute callback URLs from the browser origin, so these # never belong in a Vite build. Blank stays off (env.ts treats ""). - IAM_ENABLED=${IAM_ENABLED:-} - IAM_ISSUER=${IAM_ISSUER:-} - IAM_CLIENT_ID=${IAM_CLIENT_ID:-} - IAM_CLIENT_SECRET=${IAM_CLIENT_SECRET:-} - IAM_TENANT_ID=${IAM_TENANT_ID:-} - IAM_TENANT_SLUG=${IAM_TENANT_SLUG:-} - IAM_LOGIN_DELEGATION=${IAM_LOGIN_DELEGATION:-} - IAM_HTTP_TIMEOUT_MS=${IAM_HTTP_TIMEOUT_MS:-} - IAM_REDIRECT_URI=${IAM_REDIRECT_URI:-} - IAM_REDIRECT_URIS=${IAM_REDIRECT_URIS:-} - IAM_BROWSER_RETURN_URL=${IAM_BROWSER_RETURN_URL:-} - IAM_POST_LOGOUT_REDIRECT_URI=${IAM_POST_LOGOUT_REDIRECT_URI:-} - IAM_ALLOWED_RETURN_URLS=${IAM_ALLOWED_RETURN_URLS:-} - IAM_ADMIN_REDIRECT_URI=${IAM_ADMIN_REDIRECT_URI:-} - IAM_ADMIN_REDIRECT_URIS=${IAM_ADMIN_REDIRECT_URIS:-} - IAM_ADMIN_BROWSER_RETURN_URL=${IAM_ADMIN_BROWSER_RETURN_URL:-} - IAM_ADMIN_POST_LOGOUT_REDIRECT_URI=${IAM_ADMIN_POST_LOGOUT_REDIRECT_URI:-} - IAM_ADMIN_ALLOWED_RETURN_URLS=${IAM_ADMIN_ALLOWED_RETURN_URLS:-} - AUTH_SESSION_TRACKING=${AUTH_SESSION_TRACKING:-} # No host ports — Dokploy/Traefik routes by domain to container port 3000 expose: - "3000" # Uploaded files (product images, etc.) land in storage/uploads inside the # container. Without a volume they vanish on every recreate/redeploy, and # every UploadedFile/ProductImage row created before that would then point # at a URL that 404s. volumes: - api_uploads:/app/storage/uploads restart: unless-stopped # The bare name "api" is ambiguous on Dokploy's shared network (other stacks # register it too), which made nginx dial the wrong containers. Use a unique alias. networks: default: aliases: - goldbank-api admin-panel: build: context: ./apps/admin-panel dockerfile: Dockerfile # No host ports — Dokploy/Traefik routes by domain to container port 80 expose: - "80" restart: unless-stopped depends_on: - api user-app: build: context: ./apps/user-app dockerfile: Dockerfile # No host ports — Dokploy/Traefik routes by domain to container port 80 expose: - "80" restart: unless-stopped depends_on: - api # Same copy-only nginx pattern as user-app; map a separate Dokploy domain to this service. user-app-v2: build: context: ./apps/user-app-v2 dockerfile: Dockerfile expose: - "80" restart: unless-stopped depends_on: - api volumes: postgres_data: api_uploads: