docker-compose.yml

This commit is contained in:
ryan
2026-06-08 21:37:08 +08:00
parent 957116d983
commit ddda7c44ef
5 changed files with 364 additions and 17 deletions
+82
View File
@@ -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 * * *
+2
View File
@@ -11,6 +11,7 @@
# config
config.yaml
.env
# frontend
frontend/build
@@ -48,3 +49,4 @@ s3_cache
/internal/router/dist/
/frontend/out/
/.idea/
/uploads/
+120
View File
@@ -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
View File
@@ -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
+122
View File
@@ -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, "", " ")