# ============================================================================= # TerManager2 — production stack (Dokploy) # # The application image is self-contained: PHP dependencies and compiled # frontend assets are baked in at build time, and nginx runs alongside php-fpm # inside the app container. There are no bind mounts and no build steps at # boot, so a redeploy is: build image, migrate, serve. # # Configuration comes from the .env file, which Dokploy writes from the # variables you set in its UI. This file does not re-declare them — it only # pins the handful of values dictated by the topology below (service names, # which datastores exist), because those must match this file, not .env. # # For local development use docker-compose.dev.yml instead. # ============================================================================= x-app: &app build: context: . dockerfile: docker/php/Dockerfile restart: unless-stopped # required:false so `docker compose config` and a bare checkout still work; # a missing .env then surfaces as the explicit DB_PASSWORD error below. env_file: - path: .env required: false volumes: # Uploaded territory PDFs, thumbnails, logs and the persisted APP_KEY. - storage_data:/var/www/html/storage networks: - internal # Values that are a property of THIS compose file rather than of the # deployment, so they override whatever .env happens to say. A DB_HOST of # 127.0.0.1 left over from a local .env would otherwise break the container. x-app-env: &app-env DB_CONNECTION: mysql DB_HOST: mariadb DB_PORT: 3306 REDIS_HOST: redis REDIS_PORT: 6379 # The stack ships Redis and the worker below consumes the redis queue; # pointing these elsewhere would silently break job processing. CACHE_STORE: redis SESSION_DRIVER: redis QUEUE_CONNECTION: redis # Logs go to the container's stderr, where `docker logs` and the Dokploy # log viewer can see them, rather than into a file inside the volume. LOG_CHANNEL: stderr services: app: <<: *app environment: <<: *app-env # Sources never change in a built image, so skip the stat() on every hit. PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0 networks: - internal - dokploy-network depends_on: mariadb: condition: service_healthy redis: condition: service_healthy queue-worker: <<: *app user: www-data environment: <<: *app-env PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0 depends_on: app: condition: service_healthy entrypoint: ["queue-entrypoint.sh"] # --max-time recycles the worker hourly so a leaked handle or grown heap # can't accumulate indefinitely; restarting it is Docker's job. command: - php - artisan - queue:work - redis - --queue=default - --sleep=3 - --tries=3 - --timeout=600 - --max-time=3600 # Overrides the image's nginx-based HEALTHCHECK (no web server here). healthcheck: test: ["CMD-SHELL", "pgrep -f 'queue:work' > /dev/null || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 60s scheduler: <<: *app user: www-data environment: <<: *app-env depends_on: app: condition: service_healthy entrypoint: ["queue-entrypoint.sh"] # `schedule:work` is the container-native replacement for a host crontab — # it wakes every minute and dispatches whatever routes/console.php defines # (currently the audit-log cleanup). command: ["php", "artisan", "schedule:work"] # Same reason as above: this container runs neither nginx nor php-fpm, so # the image's HTTP health check would mark it unhealthy forever. healthcheck: test: ["CMD-SHELL", "pgrep -f 'schedule:work' > /dev/null || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 60s mariadb: image: mariadb:11 restart: unless-stopped # Interpolated (not env_file) because these are the *image's* own bootstrap # variables under different names. The `:?` guards are what make a deploy # with no configured passwords fail loudly instead of coming up with # whatever default happened to be lying around. environment: MARIADB_DATABASE: ${DB_DATABASE:-termanager2} MARIADB_USER: ${DB_USERNAME:-termanager2} MARIADB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required} MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required} MARIADB_AUTO_UPGRADE: "1" command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --innodb-buffer-pool-size=256M volumes: - db_data:/var/lib/mysql networks: - internal healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 10 start_period: 30s redis: image: redis:7-alpine restart: unless-stopped environment: REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required} command: - redis-server - --appendonly - "yes" - --requirepass - ${REDIS_PASSWORD:?REDIS_PASSWORD is required} - --maxmemory - 256mb # Sessions and cache live here; evict volatile keys rather than OOM. - --maxmemory-policy - allkeys-lru volumes: - redis_data:/data networks: - internal healthcheck: test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"] interval: 10s timeout: 5s retries: 10 start_period: 10s volumes: db_data: redis_data: storage_data: networks: # Datastores stay on an isolated network with no route to the outside. internal: driver: bridge # Dokploy's shared Traefik network. Only `app` joins it, and Dokploy attaches # the routing labels when you assign a domain to that service in the UI. dokploy-network: external: true