Docker ComposePostgreSQL 15Nginx 反向代理约 10 分钟(不含镜像构建)

从零部署一套 Umami

这份指南与 123ONE 的实际部署一致:Docker 运行 Umami + PostgreSQL,Nginx 把/Umami/反向代理到本机 3010 端口。照着做即可拥有一个完全属于自己的网站统计后台。

1. 部署架构

三个组件,一条请求链路。Umami 只监听本机回环地址,公网流量全部由 Nginx 接管:

浏览器

访问 /Umami/ 或加载采集脚本

Nginx

HTTPS 终止 + 反向代理 /Umami/

Umami :3010

Next.js 应用 + 采集 API

PostgreSQL 15 只在 Docker 内部网络暴露,不映射公网端口;数据持久化在umami-db数据卷中。

2. 环境准备

项目要求说明
操作系统Linux(amd64 / arm64)本文以 Alibaba Cloud Linux 3 为例
Docker20.10+,含 Compose v2docker compose version 能输出版本号
内存≥ 1GB 可用Umami 约 300~500MB,Postgres 约 200MB
磁盘≥ 5GB 可用镜像约 1GB,数据量取决于访问量
端口本机 3010 空闲只监听 127.0.0.1,不对外开放
域名 / 证书已配置 HTTPS本站使用 123one.online 的现有证书
如果服务器上已经有服务占用 3000 端口,不要把 Umami 映射到 3000。本指南统一使用127.0.0.1:3010,避免与现有 Node 服务冲突。

3. Docker Compose 部署

新建 /opt/umami/docker-compose.yml,内容如下。 注意 BASE_PATH 必须与最终访问路径完全一致(本站为/Umami,大写 U、无结尾斜杠)。

/opt/umami/docker-compose.yml
services:
  umami:
    # 官方镜像不支持运行时 BASE_PATH,需用 --build-arg BASE_PATH=/Umami 自建
    image: umami-123one:3.4.0
    container_name: umami
    restart: always
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    ports:
      - "127.0.0.1:3010:3000"
    volumes:
      # 构建时用 SKIP_BUILD_GEO=1 时必需,否则 /api/send 会 500
      - ./geo:/app/geo:ro
    environment:
      DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
      APP_SECRET: ${APP_SECRET}
      TWO_FACTOR_ENCRYPTION_KEY: ${TWO_FACTOR_ENCRYPTION_KEY}
      BASE_PATH: /Umami
      DISABLE_TELEMETRY: 1
      DISABLE_UPDATES: 1
      DEFAULT_LOCALE: zh-CN
      DEFAULT_CURRENCY: CNY
    depends_on:
      db:
        condition: service_healthy
    mem_limit: 768m
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/Umami/api/heartbeat || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 60s

  db:
    image: postgres:15-alpine
    container_name: umami-db
    restart: always
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      TZ: UTC
    volumes:
      - umami-db:/var/lib/postgresql/data
    mem_limit: 512m
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
      interval: 10s
      timeout: 5s
      retries: 6

volumes:
  umami-db:
为什么镜像不是官方 tag?Umami 的 BASE_PATH是构建期变量,官方预构建镜像里被固化为空字符串,运行时设置不会生效。 本站镜像使用 --build-arg BASE_PATH=/Umami自行构建;构建时跳过 GEO 的话,还必须挂载 ./geo/GeoLite2-City.mmdb, 否则采集接口会返回 500。完整步骤见仓库 deploy/umami/BUILD.md。

同目录创建 .env。三个密钥都必须是随机值,且POSTGRES_PASSWORD不能包含未转义的 @ : / ? # 等 URL 特殊字符(建议只用十六进制)。

/opt/umami/.env
# /opt/umami/.env —— 权限设为 600,不要提交到 Git
POSTGRES_PASSWORD=把这里换成随机密码
APP_SECRET=把这里换成 openssl rand -hex 32 的结果
TWO_FACTOR_ENCRYPTION_KEY=把这里换成 openssl rand -hex 32 的结果

执行初始化命令:

首次安装
# 1. 建目录
sudo mkdir -p /opt/umami && cd /opt/umami

# 2. 写入 docker-compose.yml(内容见上一节)
sudo nano docker-compose.yml

# 3. 生成 .env(三个随机值一次写入)
umask 077
{
  echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
  echo "APP_SECRET=$(openssl rand -hex 32)"
  echo "TWO_FACTOR_ENCRYPTION_KEY=$(openssl rand -hex 32)"
} > .env
chmod 600 .env

# 4. 启动
sudo docker compose up -d

# 5. 等待健康检查通过
sudo docker compose ps
curl -fsS http://127.0.0.1:3010/Umami/api/heartbeat && echo " OK"
首次启动会自动建表并创建默认账号 admin / umami。 登录后第一件事是修改密码——这一步不要拖。

4. Nginx 反向代理

在 server 配置块(443)里加入以下配置。 两个关键点:location 大小写与 BASE_PATH 一致、proxy_pass 结尾不要加斜杠。 另外不要给 /Umami 补斜杠做 301 —— Next.js 会把 /Umami/ 归一化为 /Umami,补斜杠会造成死循环。

nginx.conf
# 注意:Umami 的 BASE_PATH 是大写 /Umami
# 不要写 location = /Umami { return 301 .../Umami/; }
# Next.js 会把 /Umami/ 归一化为 /Umami,补斜杠会造成无限重定向
location ^~ /Umami {
    proxy_pass http://127.0.0.1:3010;   # 结尾不要加 /,否则路径会被剥掉
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 120s;
    proxy_buffering off;
    # 后台/登录页不需要被搜索引擎收录
    add_header X-Robots-Tag "noindex, nofollow" always;
    client_max_body_size 10m;
}

保存后检查并重载:

应用配置
# 先备份,再改配置
sudo cp /www/server/panel/vhost/nginx/120.26.18.73.conf \
        /www/server/panel/vhost/nginx/120.26.18.73.conf.bak-$(date +%F)

# 把上面的 location 粘进 server { ... } 里,然后:
sudo nginx -t && sudo nginx -s reload

# 验证(三条都应返回 200 / 301)
curl -I https://123one.online/Umami
curl -I https://123one.online/Umami/login
curl -I https://123one.online/Umami/script.js
已有站点配置时,只新增 location,不要整文件覆盖。改完必须nginx -t通过再 reload,否则可能把整站打挂。

5. 初始化后台

  1. 1
    登录后台

    打开 https://你的域名/Umami/login,默认账号 admin / umami。

  2. 2
    修改密码

    右上角头像 → 个人资料 → 修改密码。建议使用 16 位以上随机密码,并存进密码管理器。

  3. 3
    开启两步验证(可选)

    同一页面开启 2FA。前提是部署时已设置 TWO_FACTOR_ENCRYPTION_KEY,否则无法启用。

  4. 4
    添加网站

    设置 → 网站 → 添加网站,填写名称与域名。保存后复制生成的 Website ID。

  5. 5
    开启分享链接(可选)

    网站设置 → Share URL,生成只读链接,适合给客户或合作方查看。

6. 接入统计代码

把采集脚本加到你的网站。脚本地址为https://你的域名/Umami/script.js,带 data-website-id 即可。不同框架的完整代码可以在概览页的接入代码生成器里一键生成、复制。

验证清单
  • 打开网站,浏览器 Network 里能看到 POST /Umami/api/send,状态 200
  • Umami 后台「实时」页面出现当前访问
  • 页面 URL、来源、设备信息显示正确
  • 生产域名与 data-domains 配置一致

7. 日常运维

统计数据的价值在于「长期」——备份比升级更重要。建议每天自动备份,并至少每周确认一次备份文件可用。

备份
# 手动备份(输出到 /opt/umami/backup/)
cd /opt/umami
mkdir -p backup
docker exec umami-db pg_dump -U umami umami \
  | gzip > "backup/umami-$(date +%F-%H%M).sql.gz"

# 只保留最近 30 天
find backup -name "umami-*.sql.gz" -mtime +30 -delete

加入 crontab(每天 03:10):

crontab -e
# 每天凌晨 3:10 自动备份,日志写到 /var/log/umami-backup.log
10 3 * * * cd /opt/umami && mkdir -p backup && docker exec umami-db pg_dump -U umami umami | gzip > "backup/umami-$(date +\%F).sql.gz" >> /var/log/umami-backup.log 2>&1 && find backup -name "umami-*.sql.gz" -mtime +30 -delete

查看状态、升级与资源占用:

常用命令
cd /opt/umami

# 查看状态 / 日志
docker compose ps
docker compose logs -f --tail=100 umami

# 重启应用
docker compose restart umami

# 升级(先备份,再拉新镜像)
docker compose pull
docker compose up -d
docker compose ps

# 资源占用
docker stats --no-stream umami umami-db

需要恢复数据时:

恢复备份
# 恢复某个备份(会覆盖现有数据,务必先停应用)
cd /opt/umami
docker compose stop umami
gunzip -c backup/umami-2026-01-01.sql.gz \
  | docker exec -i umami-db psql -U umami -d umami
docker compose start umami
备份文件不要只留在同一块磁盘上。建议同步到对象存储或另一台服务器,避免磁盘故障时数据与备份一起丢失。

8. 故障排查

访问 /Umami 返回 502 Bad Gateway▾

原因:容器没起来、端口不对,或启动过程中数据库还没就绪。

处理:先 docker compose ps 看状态,再 docker compose logs --tail=100 umami 看报错。首次启动需要建表,通常等待 30~90 秒。若日志提示数据库连接失败,检查 .env 里的密码是否与 db 服务一致。

页面能打开,但样式和 JS 全部 404▾

原因:BASE_PATH 与 nginx 转发路径不一致,最常见的是大小写不同或 proxy_pass 多写了结尾斜杠。

处理:确认容器环境变量是 BASE_PATH=/Umami(大写 U、无结尾斜杠),nginx 用 proxy_pass http://127.0.0.1:3010;(结尾不加斜杠),且 location 前缀同样是大写 /Umami/。

登录后马上跳回登录页,或提示会话失效▾

原因:APP_SECRET 被更换、Cookie 域名不匹配,或浏览器缓存了旧的会话。

处理:APP_SECRET 一旦设置就不要随意更换。清理浏览器 Cookie 后重试;若近期改过 APP_SECRET,重新登录即可(旧会话会失效)。

后台看不到任何数据▾

原因:脚本没加载、Website ID 不对、域名被 data-domains 限制,或被广告拦截插件/CSP 拦截。

处理:打开网站控制台,看是否有 POST /Umami/api/send 请求及其状态码;确认 data-website-id 与后台一致;检查 data-domains 是否包含当前域名;确认 CSP 允许同域脚本。

统计里的时间比本地时间差 8 小时▾

原因:数据库推荐使用 UTC,看板按浏览器时区展示;如果服务器/数据库时区混乱就会出现偏差。

处理:Postgres 设置 TZ=UTC,容器内保持默认;看板右上角切换时区即可。不要在多个服务里混用 Asia/Shanghai 与 UTC。

内存不足,容器被 OOM 杀掉▾

原因:小内存服务器同时跑多个 Node 服务,或 Postgres 缓存占用过高。

处理:compose 里限制 mem_limit(本站为 umami 768m、db 512m),关闭不用的容器,必要时增加 swap 或升级内存。用 docker stats 观察峰值。

升级后无法启动 / 数据库迁移失败▾

原因:跨大版本升级未备份,或迁移过程中被中断。

处理:升级前先 pg_dump 备份。若迁移失败,回滚镜像版本(把 latest 改成具体版本号)并恢复备份,再按官方升级说明逐版本升级。

后台被大量登录尝试▾

原因:默认路径公开,存在自动化扫描。

处理:使用强密码并开启两步验证;用 fail2ban 对 /Umami/login 做频率限制;不要使用 admin/umami 默认口令。

9. 安全清单

已修改 admin 默认密码,使用 16 位以上随机密码
已开启两步验证(2FA)
APP_SECRET 唯一且未泄露,更换前了解会话会失效
Umami 端口只监听 127.0.0.1,不直接暴露公网
PostgreSQL 端口未映射到宿主机
全站强制 HTTPS,/Umami 走同一证书
DISABLE_TELEMETRY=1、DISABLE_UPDATES=1 减少外联
每日备份并定期做恢复演练
对 /Umami/login 做频率限制或 fail2ban
关注官方安全公告,及时升级镜像
需要现成的看板?

本站已经部署好 Umami,可以直接登录查看数据;也可以按照这份指南在自己的服务器上复刻一套。