diff --git a/Dockerfile b/Dockerfile index df58d003..95286f84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,9 +49,10 @@ WORKDIR /app COPY --from=builder /gochat /app/gochat COPY --from=builder /gochat-worker /app/gochat-worker COPY configs/ /app/configs/ +COPY migrations/ /app/migrations/ # Set ownership to non-root user -RUN chown -R gochat:gochat /app +RUN mkdir -p /app/storage/uploads && chown -R gochat:gochat /app # Switch to non-root user for security USER gochat diff --git a/README.md b/README.md index 1ed71879..dd28d445 100644 --- a/README.md +++ b/README.md @@ -79,13 +79,17 @@ go test -v ./tests/e2e/... ### Docker 部署 ```bash -# 构建镜像 -docker build -t gochat/gochat:latest . +# 本地/UAT 快速启动:GoChat + PostgreSQL/pgvector + Redis + Meilisearch + Mailhog +cd deploy/quickstart +cp .env.example .env +docker compose up -d --build -# 使用 docker-compose -docker compose up -d +# 可选:初始化演示账号和业务数据 +docker compose --profile seed run --rm seed ``` +详见 [deploy/quickstart/README.md](deploy/quickstart/README.md)。 + ## 环境配置 GoChat 使用 **Viper 多环境叠加**机制: @@ -188,4 +192,4 @@ GitHub Actions 5阶段流水线: ## License -MIT \ No newline at end of file +MIT diff --git a/deploy/quickstart/.env.example b/deploy/quickstart/.env.example new file mode 100644 index 00000000..b98f3bc0 --- /dev/null +++ b/deploy/quickstart/.env.example @@ -0,0 +1,35 @@ +# GoChat Docker Compose quickstart env +# Copy to .env and change secrets before sharing the stack. + +COMPOSE_PROJECT_NAME=gochat-quickstart + +# Published ports on the host +GOCHAT_PORT=3000 +MEILI_PORT=7700 +MAILHOG_SMTP_PORT=1025 +MAILHOG_WEB_PORT=8025 + +# Database +POSTGRES_DB=gochat_dev +POSTGRES_USER=gochat +POSTGRES_PASSWORD=gochat_dev + +# Redis +REDIS_PASSWORD= + +# GoChat runtime +GOCHAT_ENV=development +GOCHAT_SERVER_MODE=debug +GOCHAT_JWT_SECRET=gochat_quickstart_change_me_minimum_32_chars +GOCHAT_CAPTAIN_ENABLED=false + +# Search +MEILI_MASTER_KEY=gochat_dev +GOCHAT_SEARCH_API_KEY=gochat_dev + +# Optional deterministic seed user +GOCHAT_SEED_ADMIN_EMAIL=admin@gochat.local +GOCHAT_SEED_ADMIN_PASSWORD=changeme +GOCHAT_SEED_ADMIN_NAME=Super Admin +GOCHAT_SEED_ACCOUNT_NAME=Quickstart Account +GOCHAT_SEED_INBOX_NAME=Quickstart Website Inbox diff --git a/deploy/quickstart/README.md b/deploy/quickstart/README.md new file mode 100644 index 00000000..3d8a5c56 --- /dev/null +++ b/deploy/quickstart/README.md @@ -0,0 +1,71 @@ +# GoChat Docker Compose Quickstart + +This directory starts a local GoChat stack for manual business validation: + +- GoChat API on `http://127.0.0.1:3000` +- PostgreSQL + pgvector +- Redis +- Meilisearch +- Mailhog SMTP/Web UI +- Optional deterministic seed data + +It is intended for local/UAT quickstart, not hardened production deployment. + +## Prerequisites + +- Docker Engine with Compose v2 (`docker compose version`) +- Free local ports: `3000`, `5432`, `6379`, `7700`, `1025`, `8025` + +If those ports conflict, edit `.env` after copying `.env.example`. + +## Start + +```bash +cd deploy/quickstart +cp .env.example .env +docker compose up -d --build +``` + +Wait until GoChat is healthy: + +```bash +docker compose ps +docker compose logs -f gochat +curl -fsS http://127.0.0.1:3000/health +``` + +## Seed Demo Data + +```bash +docker compose --profile seed run --rm seed +``` + +Default login after seeding: + +- Email: `admin@gochat.local` +- Password: `changeme` + +Change these values in `.env` before running the seed command if needed. + +## Useful Commands + +```bash +# Follow all logs +docker compose logs -f + +# Restart GoChat only +docker compose restart gochat + +# Stop without deleting data +docker compose down + +# Reset all local data +docker compose down -v +``` + +## Notes + +- GoChat runs migrations automatically on startup with `GOCHAT_DATABASE_RUN_MIGRATIONS=true`. +- The production image includes `/app/migrations`; GoChat runs them automatically on startup in this quickstart stack. +- Meilisearch uses the quickstart key from `.env`; change it before sharing this stack. +- Mailhog Web UI is available at `http://127.0.0.1:8025`. diff --git a/deploy/quickstart/compose.yaml b/deploy/quickstart/compose.yaml new file mode 100644 index 00000000..12de5544 --- /dev/null +++ b/deploy/quickstart/compose.yaml @@ -0,0 +1,140 @@ +name: gochat-quickstart + +services: + postgres: + image: pgvector/pgvector:pg16 + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-gochat_dev} + POSTGRES_USER: ${POSTGRES_USER:-gochat} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-gochat_dev} + ports: + - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 20 + + 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: + - "127.0.0.1:${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: 5s + timeout: 5s + retries: 20 + + meilisearch: + image: getmeili/meilisearch:v1.13 + restart: unless-stopped + environment: + MEILI_ENV: development + MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:-gochat_dev} + MEILI_NO_ANALYTICS: "true" + ports: + - "127.0.0.1:${MEILI_PORT:-7700}:7700" + volumes: + - meili-data:/meili_data + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--spider", "http://127.0.0.1:7700/health"] + interval: 5s + timeout: 5s + retries: 30 + + mailhog: + image: mailhog/mailhog:v1.0.1 + restart: unless-stopped + ports: + - "127.0.0.1:${MAILHOG_SMTP_PORT:-1025}:1025" + - "127.0.0.1:${MAILHOG_WEB_PORT:-8025}:8025" + + gochat: + build: + context: ../.. + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + meilisearch: + condition: service_healthy + environment: &gochat-env + GOCHAT_ENV: ${GOCHAT_ENV:-development} + GOCHAT_SERVER_HOST: 0.0.0.0 + GOCHAT_SERVER_PORT: 3000 + GOCHAT_SERVER_MODE: ${GOCHAT_SERVER_MODE:-debug} + GOCHAT_DATABASE_HOST: postgres + GOCHAT_DATABASE_PORT: 5432 + GOCHAT_DATABASE_USER: ${POSTGRES_USER:-gochat} + GOCHAT_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:-gochat_dev} + GOCHAT_DATABASE_DBNAME: ${POSTGRES_DB:-gochat_dev} + GOCHAT_DATABASE_SSLMODE: disable + GOCHAT_DATABASE_RUN_MIGRATIONS: "true" + GOCHAT_DATABASE_MIGRATIONS_PATH: /app/migrations + GOCHAT_REDIS_HOST: redis + GOCHAT_REDIS_PORT: 6379 + GOCHAT_REDIS_PASSWORD: ${REDIS_PASSWORD:-} + GOCHAT_REDIS_POOL_SIZE: 50 + GOCHAT_SEARCH_ENGINE: meilisearch + GOCHAT_SEARCH_HOST: http://meilisearch:7700 + GOCHAT_SEARCH_API_KEY: ${GOCHAT_SEARCH_API_KEY:-gochat_dev} + GOCHAT_JWT_SECRET: ${GOCHAT_JWT_SECRET:-gochat_quickstart_change_me_minimum_32_chars} + GOCHAT_LOG_LEVEL: info + GOCHAT_LOG_FORMAT: json + GOCHAT_CAPTAIN_ENABLED: ${GOCHAT_CAPTAIN_ENABLED:-false} + GOCHAT_STORAGE_PROVIDER: local + GOCHAT_STORAGE_LOCAL_PATH: /app/storage/uploads + SMTP_HOST: mailhog + SMTP_PORT: 1025 + ports: + - "127.0.0.1:${GOCHAT_PORT:-3000}:3000" + volumes: + - gochat-storage:/app/storage + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"] + interval: 10s + timeout: 5s + retries: 30 + + seed: + build: + context: ../.. + dockerfile: Dockerfile + profiles: ["seed"] + depends_on: + gochat: + condition: service_healthy + environment: + <<: *gochat-env + GOCHAT_SEED_ADMIN_EMAIL: ${GOCHAT_SEED_ADMIN_EMAIL:-admin@gochat.local} + GOCHAT_SEED_ADMIN_PASSWORD: ${GOCHAT_SEED_ADMIN_PASSWORD:-changeme} + GOCHAT_SEED_ADMIN_NAME: ${GOCHAT_SEED_ADMIN_NAME:-Super Admin} + GOCHAT_SEED_ACCOUNT_NAME: ${GOCHAT_SEED_ACCOUNT_NAME:-Quickstart Account} + GOCHAT_SEED_INBOX_NAME: ${GOCHAT_SEED_INBOX_NAME:-Quickstart Website Inbox} + volumes: + - gochat-storage:/app/storage + command: ["seed"] + restart: "no" + +volumes: + postgres-data: + redis-data: + meili-data: + gochat-storage: diff --git a/internal/config/config.go b/internal/config/config.go index cb4851e9..6cf319bb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -618,36 +618,41 @@ func LoadWithEnv(env string) (*Config, error) { // Bind specific env keys that viper can't auto-infer for nested structs // These are common overrides that users set via environment variables envBindings := map[string]string{ - "GOCHAT_SERVER_HOST": "server.host", - "GOCHAT_SERVER_PORT": "server.port", - "GOCHAT_SERVER_MODE": "server.mode", - "GOCHAT_DATABASE_HOST": "database.host", - "GOCHAT_DATABASE_PORT": "database.port", - "GOCHAT_DATABASE_USER": "database.user", - "GOCHAT_DATABASE_PASSWORD": "database.password", - "GOCHAT_DATABASE_NAME": "database.name", - "GOCHAT_DATABASE_DBNAME": "database.dbname", - "GOCHAT_DATABASE_SSLMODE": "database.sslmode", - "GOCHAT_REDIS_URL": "redis.url", - "GOCHAT_REDIS_HOST": "redis.host", - "GOCHAT_REDIS_PORT": "redis.port", - "GOCHAT_REDIS_PASSWORD": "redis.password", - "GOCHAT_REDIS_POOL_SIZE": "redis.pool_size", - "GOCHAT_JWT_SECRET": "jwt.secret", - "JWT_SECRET": "jwt.secret", // Alias for compatibility (no prefix) - "GOCHAT_JWT_EXPIRY_HOURS": "jwt.expiry_hours", - "GOCHAT_LOG_LEVEL": "log.level", - "GOCHAT_LOG_FORMAT": "log.format", - "GOCHAT_CAPTAIN_ENABLED": "captain.enabled", - "GOCHAT_CAPTAIN_LLM_PROVIDER": "captain.llm_provider", - "GOCHAT_CAPTAIN_LLM_MODEL": "captain.llm_model", - "GOCHAT_CAPTAIN_LLM_API_KEY": "captain.llm_api_key", - "GOCHAT_WORKER_CONCURRENCY": "worker.concurrency", - "GOCHAT_SEARCH_ENGINE": "search.engine", - "GOCHAT_SEARCH_HOST": "search.host", - "GOCHAT_SEARCH_API_KEY": "search.api_key", - "GOCHAT_SEARCH_INDEX_PREFIX": "search.index_prefix", - "GOCHAT_SEARCH_TIMEOUT_SECONDS": "search.timeout_seconds", + "GOCHAT_SERVER_HOST": "server.host", + "GOCHAT_SERVER_PORT": "server.port", + "GOCHAT_SERVER_MODE": "server.mode", + "GOCHAT_DATABASE_HOST": "database.host", + "GOCHAT_DATABASE_PORT": "database.port", + "GOCHAT_DATABASE_USER": "database.user", + "GOCHAT_DATABASE_PASSWORD": "database.password", + "GOCHAT_DATABASE_NAME": "database.name", + "GOCHAT_DATABASE_DBNAME": "database.dbname", + "GOCHAT_DATABASE_SSLMODE": "database.sslmode", + "GOCHAT_DATABASE_RUN_MIGRATIONS": "database.run_migrations", + "GOCHAT_DATABASE_MIGRATIONS_PATH": "database.migrations_path", + "GOCHAT_REDIS_URL": "redis.url", + "GOCHAT_REDIS_HOST": "redis.host", + "GOCHAT_REDIS_PORT": "redis.port", + "GOCHAT_REDIS_PASSWORD": "redis.password", + "GOCHAT_REDIS_POOL_SIZE": "redis.pool_size", + "GOCHAT_JWT_SECRET": "jwt.secret", + "JWT_SECRET": "jwt.secret", // Alias for compatibility (no prefix) + "GOCHAT_JWT_EXPIRY_HOURS": "jwt.expiry_hours", + "GOCHAT_LOG_LEVEL": "log.level", + "GOCHAT_LOG_FORMAT": "log.format", + "GOCHAT_CAPTAIN_ENABLED": "captain.enabled", + "GOCHAT_CAPTAIN_LLM_PROVIDER": "captain.llm_provider", + "GOCHAT_CAPTAIN_LLM_MODEL": "captain.llm_model", + "GOCHAT_CAPTAIN_LLM_API_KEY": "captain.llm_api_key", + "GOCHAT_WORKER_CONCURRENCY": "worker.concurrency", + "GOCHAT_SEARCH_ENGINE": "search.engine", + "GOCHAT_SEARCH_HOST": "search.host", + "GOCHAT_SEARCH_API_KEY": "search.api_key", + "GOCHAT_SEARCH_INDEX_PREFIX": "search.index_prefix", + "GOCHAT_SEARCH_TIMEOUT_SECONDS": "search.timeout_seconds", + "GOCHAT_STORAGE_PROVIDER": "storage.provider", + "GOCHAT_STORAGE_LOCAL_PATH": "storage.local_path", + "GOCHAT_STORAGE_MAX_FILE_SIZE": "storage.max_file_size", // G10: OAuth config for new channel integrations (Twitter, Microsoft, Google) "GOCHAT_OAUTH_TWITTER_CLIENT_ID": "oauth.twitter.client_id", "GOCHAT_OAUTH_TWITTER_CLIENT_SECRET": "oauth.twitter.client_secret",