services: mysql: image: mariadb:11.4 restart: unless-stopped environment: MARIADB_DATABASE: ${MYSQL_DATABASE:-blog} MARIADB_USER: ${MYSQL_USER:-blog} MARIADB_PASSWORD: ${MYSQL_PASSWORD:-blog123456} MARIADB_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123456} command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci ports: - '${MYSQL_PORT:-3306}:3306' volumes: - mysql_data:/var/lib/mysql healthcheck: test: [ 'CMD-SHELL', 'mariadb-admin ping -h 127.0.0.1 -u root -p$${MARIADB_ROOT_PASSWORD} --silent', ] interval: 10s timeout: 5s retries: 12 start_period: 20s networks: - blog-network redis: image: redis:7-alpine restart: unless-stopped command: [ 'sh', '-c', 'if [ -n "$$REDIS_PASSWORD" ]; then exec redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD"; else exec redis-server --appendonly yes; fi', ] environment: REDIS_PASSWORD: ${REDIS_PASSWORD:-} ports: - '${REDIS_PORT:-6379}:6379' volumes: - redis_data:/data healthcheck: test: [ 'CMD-SHELL', 'if [ -n "$$REDIS_PASSWORD" ]; then redis-cli -a "$$REDIS_PASSWORD" ping; else redis-cli ping; fi', ] interval: 10s timeout: 5s retries: 12 networks: - blog-network meilisearch: image: getmeili/meilisearch:v1.7 restart: unless-stopped environment: MEILI_MASTER_KEY: ${MEILI_MASTER_KEY} MEILI_ENV: production MEILI_NO_ANALYTICS: 'true' ports: - '${MEILI_PORT:-7700}:7700' volumes: - meilisearch_data:/meili_data healthcheck: test: ['CMD', 'wget', '--no-verbose', '--spider', 'http://127.0.0.1:7700/health'] interval: 10s timeout: 5s retries: 12 start_period: 10s networks: - blog-network admin-server-migrate: build: context: . dockerfile: Dockerfile.compose target: admin-server-tools restart: 'no' depends_on: mysql: condition: service_healthy environment: APP_ENV: ${APP_ENV} NODE_ENV: production DATABASE_URL: ${DATABASE_URL} command: ['pnpm', '--filter', '@blog/db', 'db:migrate:deploy'] networks: - blog-network admin-search-init: build: context: . dockerfile: Dockerfile.compose target: admin-server-tools restart: 'no' depends_on: mysql: condition: service_healthy redis: condition: service_healthy admin-server-migrate: condition: service_completed_successfully admin-server-seed: condition: service_completed_successfully environment: APP_ENV: ${APP_ENV} NODE_ENV: production JWT_SECRET: ${JWT_SECRET} DATABASE_URL: ${DATABASE_URL} BLOG_PUBLIC_URL: ${BLOG_PUBLIC_URL:-http://localhost:3000} ADMIN_PUBLIC_URL: ${ADMIN_PUBLIC_URL:-http://localhost:8080} API_PUBLIC_URL: ${API_PUBLIC_URL:-http://localhost:8000} REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-} MEILI_URL: ${MEILI_URL:-} MEILI_ADMIN_KEY: ${MEILI_ADMIN_KEY:-} command: ['node', '/app/apps/admin/server/dist/scripts/init-meilisearch.js'] networks: - blog-network admin-server-seed: build: context: . dockerfile: Dockerfile.compose target: admin-server-tools restart: 'no' depends_on: mysql: condition: service_healthy admin-server-migrate: condition: service_completed_successfully environment: APP_ENV: ${APP_ENV} NODE_ENV: production DATABASE_URL: ${DATABASE_URL} command: ['node', '/app/apps/admin/server/dist/scripts/seed-database-if-empty.js'] networks: - blog-network admin-server: build: context: . dockerfile: Dockerfile.compose target: admin-server-runner restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_healthy admin-server-migrate: condition: service_completed_successfully admin-server-seed: condition: service_completed_successfully environment: APP_ENV: ${APP_ENV} NODE_ENV: production PORT: 8000 JWT_SECRET: ${JWT_SECRET} DATABASE_URL: ${DATABASE_URL} REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-} MEILI_URL: ${MEILI_URL:-} MEILI_ADMIN_KEY: ${MEILI_ADMIN_KEY:-} BLOG_PUBLIC_URL: ${BLOG_PUBLIC_URL:-http://localhost:3000} ADMIN_PUBLIC_URL: ${ADMIN_PUBLIC_URL:-http://localhost:8080} API_PUBLIC_URL: ${API_PUBLIC_URL:-http://localhost:8000} ACCESS_TOKEN_TTL_SECONDS: ${ACCESS_TOKEN_TTL_SECONDS:-} SESSION_TTL_SECONDS: ${SESSION_TTL_SECONDS:-} CAPTCHA_TTL_SECONDS: ${CAPTCHA_TTL_SECONDS:-} AUTH_SESSION_COOKIE_NAME: ${AUTH_SESSION_COOKIE_NAME:-} SESSION_COOKIE_MAX_AGE_MS: ${SESSION_COOKIE_MAX_AGE_MS:-} CAPTCHA_COOKIE_NAME: ${CAPTCHA_COOKIE_NAME:-} CAPTCHA_COOKIE_MAX_AGE_MS: ${CAPTCHA_COOKIE_MAX_AGE_MS:-} COOKIE_SECURE: ${COOKIE_SECURE:-} COOKIE_DOMAIN: ${COOKIE_DOMAIN:-} COOKIE_SECURE_PROXY_HOSTS: ${COOKIE_SECURE_PROXY_HOSTS:-} ADMIN_ALLOWED_ORIGINS: ${ADMIN_ALLOWED_ORIGINS:-http://localhost:8080,http://127.0.0.1:8080} IP2REGION_XDB_PATH: ${IP2REGION_XDB_PATH:-} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-} SMTP_SECURE: ${SMTP_SECURE:-} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-} EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-} COS_SECRET_ID: ${COS_SECRET_ID:-} COS_SECRET_KEY: ${COS_SECRET_KEY:-} COS_BUCKET: ${COS_BUCKET:-} COS_REGION: ${COS_REGION:-} COS_CUSTOM_DOMAIN: ${COS_CUSTOM_DOMAIN:-} COS_SLICE_SIZE: ${COS_SLICE_SIZE:-} GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-} GITHUB_NEXT_CLIENT_ID: ${GITHUB_NEXT_CLIENT_ID:-} GITHUB_NEXT_CLIENT_SECRET: ${GITHUB_NEXT_CLIENT_SECRET:-} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} GOOGLE_NEXT_CLIENT_ID: ${GOOGLE_NEXT_CLIENT_ID:-} GOOGLE_NEXT_CLIENT_SECRET: ${GOOGLE_NEXT_CLIENT_SECRET:-} GOOGLE_OAUTH_PROXY_URL: ${GOOGLE_OAUTH_PROXY_URL:-} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} DEEPSEEK_BASE_URL: ${DEEPSEEK_BASE_URL:-} DASHBOARD_PUSH_INTERVAL_MS: ${DASHBOARD_PUSH_INTERVAL_MS:-15000} METRICS_FLUSH_INTERVAL_MS: ${METRICS_FLUSH_INTERVAL_MS:-60000} OTEL_ENABLED: ${OTEL_ENABLED:-} OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-} OTEL_SERVICE_VERSION: ${OTEL_SERVICE_VERSION:-} OTEL_ENVIRONMENT: ${OTEL_ENVIRONMENT:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_TRACES_SAMPLE_RATE_PERCENT: ${OTEL_TRACES_SAMPLE_RATE_PERCENT:-} ports: - '${ADMIN_SERVER_PORT:-8000}:8000' healthcheck: test: [ 'CMD', 'node', '-e', "require('node:http').get('http://127.0.0.1:8000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));", ] interval: 15s timeout: 5s retries: 12 start_period: 20s networks: - blog-network admin-metrics-worker: build: context: . dockerfile: Dockerfile.compose target: admin-server-runner command: ['node', '/app/apps/admin/server/dist/scripts/flush-metrics.js'] restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_healthy admin-server-migrate: condition: service_completed_successfully admin-server-seed: condition: service_completed_successfully environment: APP_ENV: ${APP_ENV} NODE_ENV: production JWT_SECRET: ${JWT_SECRET} DATABASE_URL: ${DATABASE_URL} BLOG_PUBLIC_URL: ${BLOG_PUBLIC_URL:-http://localhost:3000} ADMIN_PUBLIC_URL: ${ADMIN_PUBLIC_URL:-http://localhost:8080} API_PUBLIC_URL: ${API_PUBLIC_URL:-http://localhost:8000} REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-} METRICS_FLUSH_INTERVAL_MS: ${METRICS_FLUSH_INTERVAL_MS:-60000} networks: - blog-network admin-client: build: context: . dockerfile: Dockerfile.compose target: admin-client-runner args: VITE_GITHUB_ID: ${VITE_GITHUB_ID:-} VITE_GOOGLE_ID: ${VITE_GOOGLE_ID:-} BLOG_PUBLIC_URL: ${BLOG_PUBLIC_URL:-http://localhost:3000} ADMIN_PUBLIC_URL: ${ADMIN_PUBLIC_URL:-http://localhost:8080} API_PUBLIC_URL: ${API_PUBLIC_URL:-http://localhost:8000} restart: unless-stopped depends_on: admin-server: condition: service_healthy ports: - '${ADMIN_CLIENT_PORT:-8080}:80' healthcheck: test: ['CMD', 'wget', '--no-verbose', '--spider', 'http://127.0.0.1/'] interval: 15s timeout: 5s retries: 12 networks: - blog-network blog: build: context: . dockerfile: Dockerfile.compose target: blog-runner extra_hosts: - 'host.docker.internal:host-gateway' args: BLOG_PUBLIC_URL: ${BLOG_PUBLIC_URL:-http://localhost:3000} ADMIN_PUBLIC_URL: ${ADMIN_PUBLIC_URL:-http://localhost:8080} API_PUBLIC_URL: ${API_PUBLIC_URL:-http://localhost:8000} DATABASE_URL: ${BLOG_BUILD_DATABASE_URL} GITHUB_TOKEN: ${GITHUB_TOKEN:-} NEXT_PUBLIC_GITHUB_CLIENT_ID: ${NEXT_PUBLIC_GITHUB_CLIENT_ID:-} NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${NEXT_PUBLIC_GOOGLE_CLIENT_ID:-} restart: unless-stopped extra_hosts: - 'host.docker.internal:host-gateway' depends_on: mysql: condition: service_healthy redis: condition: service_healthy admin-server: condition: service_healthy environment: APP_ENV: ${APP_ENV} BLOG_PUBLIC_URL: ${BLOG_PUBLIC_URL:-http://localhost:3000} ADMIN_PUBLIC_URL: ${ADMIN_PUBLIC_URL:-http://localhost:8080} API_PUBLIC_URL: ${API_PUBLIC_URL:-http://localhost:8000} DATABASE_URL: ${DATABASE_URL} REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-} MEILI_URL: ${MEILI_URL:-} MEILI_SEARCH_KEY: ${MEILI_SEARCH_KEY:-} GITHUB_TOKEN: ${GITHUB_TOKEN:-} ports: - '${BLOG_PORT:-3000}:3000' healthcheck: test: [ 'CMD', 'node', '-e', "require('node:http').get('http://127.0.0.1:3000', (res) => { process.exit(res.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1));", ] interval: 15s timeout: 5s retries: 12 start_period: 20s networks: - blog-network volumes: mysql_data: driver: local redis_data: driver: local meilisearch_data: driver: local networks: blog-network: driver: bridge