mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
docker-compose.yml
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# wavelet — 环境变量配置模板
|
||||
# 复制此文件为 .env 并填入实际值: cp .env.example .env
|
||||
# 环境变量优先级高于 config.yaml / config.docker.yaml
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ─── Docker Compose 服务端口映射 ───────────────────────────────────────────────
|
||||
APP_PORT=8000
|
||||
POSTGRES_PORT=5432
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ─── PostgreSQL 容器配置(仅 docker-compose 使用)────────────────────────────
|
||||
POSTGRES_DB=wavelet
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
|
||||
# ─── 时区 ─────────────────────────────────────────────────────────────────────
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# ─── 应用配置 ──────────────────────────────────────────────────────────────────
|
||||
APP_NAME=wavelet
|
||||
APP_ENV=production
|
||||
APP_ADDR=:8000
|
||||
APP_NODE_ID=1
|
||||
APP_API_PREFIX=/api
|
||||
# APP_GRACEFUL_SHUTDOWN_TIMEOUT=30
|
||||
APP_SESSION_COOKIE_NAME=wavelet_session_id
|
||||
APP_SESSION_SECRET=change-me-to-a-random-string-in-production
|
||||
# APP_SESSION_DOMAIN=
|
||||
# APP_SESSION_AGE=86400
|
||||
APP_SESSION_HTTP_ONLY=true
|
||||
APP_SESSION_SECURE=true
|
||||
|
||||
# ─── 数据库 ────────────────────────────────────────────────────────────────────
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=wavelet
|
||||
DB_SSL_MODE=disable
|
||||
DB_TIMEZONE=Asia/Shanghai
|
||||
# DB_LOG_LEVEL=info
|
||||
# DB_MAX_IDLE_CONN=16
|
||||
# DB_MAX_OPEN_CONN=128
|
||||
|
||||
# ─── Redis ─────────────────────────────────────────────────────────────────────
|
||||
REDIS_ADDR=redis:6379
|
||||
# REDIS_USERNAME=
|
||||
# REDIS_PASSWORD=
|
||||
# REDIS_DB=0
|
||||
REDIS_KEY_PREFIX=wavelet:
|
||||
# REDIS_POOL_SIZE=100
|
||||
|
||||
# ─── ClickHouse(可选)───────────────────────────────────────────────────────
|
||||
# CLICKHOUSE_HOST=clickhouse:9000
|
||||
# CLICKHOUSE_USERNAME=default
|
||||
# CLICKHOUSE_PASSWORD=
|
||||
# CLICKHOUSE_NAME=wavelet
|
||||
|
||||
# ─── 日志 ──────────────────────────────────────────────────────────────────────
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
LOG_OUTPUT=stdout
|
||||
|
||||
# ─── OpenTelemetry ─────────────────────────────────────────────────────────────
|
||||
# 设为 0 关闭 tracing(无 Collector 时推荐)
|
||||
OTEL_SAMPLING_RATE=0.0
|
||||
|
||||
# ─── S3 兼容存储(可选)───────────────────────────────────────────────────────
|
||||
# S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
|
||||
# S3_REGION=auto
|
||||
# S3_BUCKET=<bucket-name>
|
||||
# S3_ACCESS_KEY_ID=<access-key>
|
||||
# S3_SECRET_ACCESS_KEY=<secret-key>
|
||||
# S3_PATH_STYLE=false
|
||||
# S3_CDN_URL=
|
||||
|
||||
# ─── Worker ────────────────────────────────────────────────────────────────────
|
||||
# WORKER_CONCURRENCY=20
|
||||
|
||||
# ─── Scheduler ─────────────────────────────────────────────────────────────────
|
||||
# SCHEDULER_CLEANUP_CRON=0 */2 * * *
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
# config
|
||||
config.yaml
|
||||
.env
|
||||
|
||||
# frontend
|
||||
frontend/build
|
||||
@@ -48,3 +49,4 @@ s3_cache
|
||||
/internal/router/dist/
|
||||
/frontend/out/
|
||||
/.idea/
|
||||
/uploads/
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# wavelet — Docker Compose 专用配置
|
||||
# 此文件供 docker-compose 挂载使用,主机名指向 Docker 服务名
|
||||
# 如需自定义,请复制此文件并修改,然后在 docker-compose.yml 中更新挂载路径
|
||||
|
||||
# ─── Application ────────────────────────────────────────────────────────────────
|
||||
app:
|
||||
app_name: "wavelet"
|
||||
env: "production"
|
||||
addr: ":8000"
|
||||
node_id: 1
|
||||
graceful_shutdown_timeout: 30
|
||||
session_cookie_name: "wavelet_session_id"
|
||||
session_secret: "change-me-to-a-random-string-in-production"
|
||||
session_domain: ""
|
||||
session_age: 86400
|
||||
session_secure: true
|
||||
session_http_only: true
|
||||
api_prefix: "/api"
|
||||
|
||||
# ─── PostgreSQL ─────────────────────────────────────────────────────────────────
|
||||
database:
|
||||
enabled: true
|
||||
host: "postgres" # Docker 服务名
|
||||
port: 5432
|
||||
username: "postgres"
|
||||
password: "postgres"
|
||||
database: "wavelet"
|
||||
max_idle_conn: 16
|
||||
max_open_conn: 128
|
||||
conn_max_lifetime: 1800
|
||||
conn_max_idle_time: 600
|
||||
log_level: "info"
|
||||
ssl_mode: "disable"
|
||||
time_zone: "Asia/Shanghai"
|
||||
application_name: "wavelet-server"
|
||||
prefer_simple_protocol: false
|
||||
search_path: "public"
|
||||
statement_cache_capacity: 256
|
||||
default_query_exec_mode: "cache_statement"
|
||||
slow_threshold: 200ms
|
||||
|
||||
# ─── Redis ──────────────────────────────────────────────────────────────────────
|
||||
redis:
|
||||
enabled: true
|
||||
addrs:
|
||||
- "redis:6379" # Docker 服务名
|
||||
username: ""
|
||||
password: ""
|
||||
db: 0
|
||||
cluster_mode: false
|
||||
master_name: ""
|
||||
key_prefix: "wavelet:"
|
||||
pool_size: 100
|
||||
min_idle_conn: 10
|
||||
dial_timeout: 5
|
||||
read_timeout: 3
|
||||
write_timeout: 3
|
||||
max_retries: 3
|
||||
pool_timeout: 4
|
||||
conn_max_idle_time: 300
|
||||
|
||||
# ─── ClickHouse (optional) ──────────────────────────────────────────────────────
|
||||
clickhouse:
|
||||
enabled: false
|
||||
hosts:
|
||||
- "clickhouse:9000"
|
||||
username: "default"
|
||||
password: ""
|
||||
database: "wavelet"
|
||||
max_idle_conn: 10
|
||||
max_open_conn: 100
|
||||
conn_max_lifetime: 3600
|
||||
dial_timeout: 5
|
||||
block_buffer_size: 10
|
||||
|
||||
# ─── Logging ────────────────────────────────────────────────────────────────────
|
||||
log:
|
||||
level: "info"
|
||||
format: "json"
|
||||
output: "stdout"
|
||||
file_path: ""
|
||||
max_size: 100
|
||||
max_age: 30
|
||||
max_backups: 10
|
||||
compress: true
|
||||
|
||||
# ─── Task Scheduler (Cron) ──────────────────────────────────────────────────────
|
||||
scheduler:
|
||||
cleanup_unused_uploads_task_cron: "0 */2 * * *"
|
||||
|
||||
# ─── Async Task Worker ──────────────────────────────────────────────────────────
|
||||
worker:
|
||||
concurrency: 20
|
||||
strict_priority: false
|
||||
queues:
|
||||
- name: webhook
|
||||
priority: 10
|
||||
- name: whitelist_only
|
||||
priority: 5
|
||||
- name: default
|
||||
priority: 3
|
||||
|
||||
# ─── OpenTelemetry Tracing ──────────────────────────────────────────────────────
|
||||
otel:
|
||||
sampling_rate: 0.0 # Docker 环境默认关闭 tracing,避免无 Collector 时报错
|
||||
|
||||
# ─── S3-Compatible File Storage ─────────────────────────────────────────────────
|
||||
s3:
|
||||
enabled: false
|
||||
endpoint: ""
|
||||
region: "auto"
|
||||
bucket: ""
|
||||
access_key_id: ""
|
||||
secret_access_key: ""
|
||||
path_style: false
|
||||
key_prefix: ""
|
||||
cdn_url: ""
|
||||
local_cache:
|
||||
enabled: false
|
||||
cache_dir: "./s3_cache"
|
||||
+38
-17
@@ -1,4 +1,25 @@
|
||||
services:
|
||||
|
||||
wavelet:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
container_name: wavelet-app
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
TZ: ${TZ:-Asia/Shanghai}
|
||||
ports:
|
||||
- "${APP_PORT:-8000}:8000"
|
||||
volumes:
|
||||
- ./config.docker.yaml:/app/config.yaml:ro
|
||||
- ./uploads:/app/uploads
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: wavelet-postgres
|
||||
@@ -18,23 +39,23 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
#
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: wavelet-redis
|
||||
# restart: unless-stopped
|
||||
# command: ["redis-server", "--appendonly", "yes"]
|
||||
# ports:
|
||||
# - "${REDIS_PORT:-6379}:6379"
|
||||
# volumes:
|
||||
# - ./data/redis_data:/data
|
||||
# healthcheck:
|
||||
# test: ["CMD", "redis-cli", "ping"]
|
||||
# interval: 10s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
# start_period: 5s
|
||||
#
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: wavelet-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
volumes:
|
||||
- ./data/redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
|
||||
# clickhouse:
|
||||
# image: clickhouse/clickhouse-server:25.3-alpine
|
||||
# container_name: wavelet-clickhouse
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
@@ -80,6 +81,9 @@ func init() {
|
||||
log.Fatalf("[Config] parse config failed: %v\n", err)
|
||||
}
|
||||
|
||||
// 环境变量覆盖(优先级高于 config.yaml)
|
||||
applyEnvOverrides(&c)
|
||||
|
||||
// Disable standard DB/Redis initializations during tests to prevent connection attempts.
|
||||
if isTest() {
|
||||
c.Database.Enabled = false
|
||||
@@ -94,6 +98,124 @@ func init() {
|
||||
printConfig(&c)
|
||||
}
|
||||
|
||||
// ─── 环境变量覆盖层 ────────────────────────────────────────────────────────────
|
||||
// 环境变量优先级高于 config.yaml,未设置则保留 yaml 中的值。
|
||||
|
||||
func envStr(key, fallback string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envInt(key string, fallback int) int {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envInt64(key string, fallback int64) int64 {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envFloat64(key string, fallback float64) float64 {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if n, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envBool(key string, fallback bool) bool {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// applyEnvOverrides 将环境变量值覆盖到配置结构体上(仅当环境变量已设置时生效)
|
||||
func applyEnvOverrides(c *configModel) {
|
||||
// ─── App ───
|
||||
c.App.AppName = envStr("APP_NAME", c.App.AppName)
|
||||
c.App.Env = envStr("APP_ENV", c.App.Env)
|
||||
c.App.Addr = envStr("APP_ADDR", c.App.Addr)
|
||||
c.App.NodeID = envInt64("APP_NODE_ID", c.App.NodeID)
|
||||
c.App.APIPrefix = envStr("APP_API_PREFIX", c.App.APIPrefix)
|
||||
c.App.GracefulShutdownTimeout = envInt("APP_GRACEFUL_SHUTDOWN_TIMEOUT", c.App.GracefulShutdownTimeout)
|
||||
c.App.SessionCookieName = envStr("APP_SESSION_COOKIE_NAME", c.App.SessionCookieName)
|
||||
c.App.SessionSecret = envStr("APP_SESSION_SECRET", c.App.SessionSecret)
|
||||
c.App.SessionDomain = envStr("APP_SESSION_DOMAIN", c.App.SessionDomain)
|
||||
c.App.SessionAge = envInt("APP_SESSION_AGE", c.App.SessionAge)
|
||||
c.App.SessionHttpOnly = envBool("APP_SESSION_HTTP_ONLY", c.App.SessionHttpOnly)
|
||||
c.App.SessionSecure = envBool("APP_SESSION_SECURE", c.App.SessionSecure)
|
||||
|
||||
// ─── Database ───
|
||||
c.Database.Host = envStr("DB_HOST", c.Database.Host)
|
||||
c.Database.Port = envInt("DB_PORT", c.Database.Port)
|
||||
c.Database.Username = envStr("DB_USERNAME", c.Database.Username)
|
||||
c.Database.Password = envStr("DB_PASSWORD", c.Database.Password)
|
||||
c.Database.Database = envStr("DB_NAME", c.Database.Database)
|
||||
c.Database.SSLMode = envStr("DB_SSL_MODE", c.Database.SSLMode)
|
||||
c.Database.TimeZone = envStr("DB_TIMEZONE", c.Database.TimeZone)
|
||||
c.Database.LogLevel = envStr("DB_LOG_LEVEL", c.Database.LogLevel)
|
||||
c.Database.MaxIdleConn = envInt("DB_MAX_IDLE_CONN", c.Database.MaxIdleConn)
|
||||
c.Database.MaxOpenConn = envInt("DB_MAX_OPEN_CONN", c.Database.MaxOpenConn)
|
||||
|
||||
// ─── Redis ───
|
||||
if v, ok := os.LookupEnv("REDIS_ADDR"); ok {
|
||||
c.Redis.Addrs = []string{v}
|
||||
}
|
||||
c.Redis.Username = envStr("REDIS_USERNAME", c.Redis.Username)
|
||||
c.Redis.Password = envStr("REDIS_PASSWORD", c.Redis.Password)
|
||||
c.Redis.DB = envInt("REDIS_DB", c.Redis.DB)
|
||||
c.Redis.KeyPrefix = envStr("REDIS_KEY_PREFIX", c.Redis.KeyPrefix)
|
||||
c.Redis.PoolSize = envInt("REDIS_POOL_SIZE", c.Redis.PoolSize)
|
||||
|
||||
// ─── ClickHouse ───
|
||||
if v, ok := os.LookupEnv("CLICKHOUSE_HOST"); ok {
|
||||
c.ClickHouse.Hosts = []string{v}
|
||||
}
|
||||
c.ClickHouse.Username = envStr("CLICKHOUSE_USERNAME", c.ClickHouse.Username)
|
||||
c.ClickHouse.Password = envStr("CLICKHOUSE_PASSWORD", c.ClickHouse.Password)
|
||||
c.ClickHouse.Database = envStr("CLICKHOUSE_NAME", c.ClickHouse.Database)
|
||||
|
||||
// ─── Log ───
|
||||
c.Log.Level = envStr("LOG_LEVEL", c.Log.Level)
|
||||
c.Log.Format = envStr("LOG_FORMAT", c.Log.Format)
|
||||
c.Log.Output = envStr("LOG_OUTPUT", c.Log.Output)
|
||||
|
||||
// ─── OTel ───
|
||||
c.Otel.SamplingRate = envFloat64("OTEL_SAMPLING_RATE", c.Otel.SamplingRate)
|
||||
|
||||
// ─── S3 ───
|
||||
c.S3.Endpoint = envStr("S3_ENDPOINT", c.S3.Endpoint)
|
||||
c.S3.Region = envStr("S3_REGION", c.S3.Region)
|
||||
c.S3.Bucket = envStr("S3_BUCKET", c.S3.Bucket)
|
||||
c.S3.AccessKeyID = envStr("S3_ACCESS_KEY_ID", c.S3.AccessKeyID)
|
||||
c.S3.SecretAccessKey = envStr("S3_SECRET_ACCESS_KEY", c.S3.SecretAccessKey)
|
||||
c.S3.CdnURL = envStr("S3_CDN_URL", c.S3.CdnURL)
|
||||
c.S3.PathStyle = envBool("S3_PATH_STYLE", c.S3.PathStyle)
|
||||
|
||||
// ─── Worker ───
|
||||
c.Worker.Concurrency = envInt("WORKER_CONCURRENCY", c.Worker.Concurrency)
|
||||
|
||||
// ─── Scheduler ───
|
||||
c.Scheduler.CleanupUnusedUploadsTaskCron = envStr(
|
||||
"SCHEDULER_CLEANUP_CRON", c.Scheduler.CleanupUnusedUploadsTaskCron,
|
||||
)
|
||||
}
|
||||
|
||||
// printConfig 打印配置内容
|
||||
func printConfig(c *configModel) {
|
||||
configJSON, err := json.MarshalIndent(c, "", " ")
|
||||
|
||||
Reference in New Issue
Block a user