GoatCounter 2.7.0 Docker Compose 部署指南

在 Docker Compose 上部署 GoatCounter 2.7.0 的完整实测指南:compose 全文逐项说明、SQLite 数据持久化、Web 向导与 CLI 双路首次初始化、Host 域名校验与端口映射排障实录、等价 docker run 命令及 2.6→2.7 升级回滚演练。

GoatCounter自托管GoSQLite监控LinuxGoatCounter2026/11/01

GoatCounter 2.7.0 Docker Compose 部署指南

目标:在一台 Linux 服务器上用 Docker Compose 把 GoatCounter 2.7.0 部署为常驻统计服务,覆盖 compose 文件编写、首次启动、站点与管理员初始化(Web 向导与 CLI 两种方式)、数据持久化、版本升级与排障实录。全文命令与回显均在 Ubuntu 26.04.1 LTS 实测通过(实测日期:2026-11-01),可逐条照做复现。

一、环境信息

实测服务器基线(回显照抄):

$ uname -a
Linux master 7.0.0-38-generic #38-Ubuntu SMP PREEMPT_DYNAMIC Fri Sep  4 09:10:14 UTC 2026 x86_64 GNU/Linux
$ grep PRETTY /etc/os-release
PRETTY_NAME="Ubuntu 26.04.1 LTS"
$ docker --version
Docker version 29.9.2, build a82e528
$ docker compose version
Docker Compose version v5.6.1
项目值说明
操作系统Ubuntu 26.04.1 LTS任何能跑 Docker 的 x86_64 Linux 均可
Docker Engine29.9.2含 Compose v2 插件(docker compose 子命令)
Docker Composev5.6.1本文统一使用 docker compose(空格拼写)
GoatCounter 镜像arp242/goatcounter:2.7.0(56.2MB,ID 791df3317bf5)官方镜像,2026-11-01 复核为最新稳定 tag
监听端口8141部署完成后宿主机 0.0.0.0:8141 监听(实测 ss -ltn):LISTEN 0.0.0.0:8141
站点域名stats.example.comGoatCounter 按域名路由站点,本文全程用该示例域名
数据目录/opt/goatcounter/data(容器内 /home/goatcounter/goatcounter-data)bind mount 持久化 SQLite 数据库
管理员账号admin@example.com / q1w2e3r4首次初始化时通过向导或 CLI 创建

二、镜像选型说明

GoatCounter 的官方分发形态有三种:GitHub Releases 预编译二进制(见《Ubuntu 24.04 安装 GoatCounter 2.7.0 指南》)、源码编译、以及作者在 Docker Hub 维护的官方镜像 arp242/goatcounter。v2.7.0 Release Notes 明确标注 "Also available on DockerHub: https://hub.docker.com/r/arp242/goatcounter"(2026-11-01 实查 GitHub Releases,v2.7.0 为最新稳定版,发布于 2025-12-15)。

镜像实测元数据:

$ docker image inspect arp242/goatcounter:2.7.0 \
    --format 'Size={{.Size}} Exposed={{.Config.ExposedPorts}} Entrypoint={{.Config.Entrypoint}} Cmd={{.Config.Cmd}} User={{.Config.User}} WorkingDir={{.Config.WorkingDir}}'
Size=56196505 Exposed=map[443/tcp:{} 80/tcp:{} 8080/tcp:{}] Entrypoint=[goatcounter] Cmd=[serve -automigrate] User=goatcounter:goatcounter WorkingDir=/home/goatcounter
特性值部署影响
镜像体积56,196,505 字节(约 56.2MB)单镜像即可运行,无额外依赖镜像
入口Entrypoint=[goatcounter],Cmd=[serve -automigrate]不写 command: 时默认就是 serve -automigrate;本文显式写全 command 以固定端口与数据库路径
运行用户goatcounter:goatcounter(容器内实测 id:uid=1000(goatcounter) gid=1000(goatcounter))非 root 运行,bind mount 数据目录属主必须是 uid 1000(排障见九、9.2)
声明端口EXPOSE 80/443/80808080 是镜像内默认监听端口(-listen 默认 *:8080);80/443 对应 TLS 部署形态。声明仅作文档提示,实际端口由 command: 决定
GeoIP 库内置 GeoLite2-Country(启动日志实测 geoip.build = 2025-05-20 09:30:47、geoip.nodes = 1522374)无需额外配置即可按国家聚合访问来源

⚠️ 版本自述差异:官方 Docker 镜像内 goatcounter version 输出 version=7e91d8a9bdbb_2025-12-15T18:20:31Z-dev; go=go1.25.5; GOOS=linux; GOARCH=amd64; race=false; cgo=true(构建 commit 串),而 GitHub Releases 的官方二进制输出 version=v2.7.0; ...。两者同属 v2.7.0 tag 的发行物,只是版本自述串的生成方式不同,功能一致,不以此判断"装错了版本"。

💡 技术拓展:GoatCounter 支持两种后端数据库,部署形态随之分岔——

后端连接方式适用场景
SQLite-db sqlite+<文件路径>单机部署、中小流量;单文件库,备份即拷文件
PostgreSQL-db postgres+<connect串>大流量、多实例共享库;需要额外部署数据库

官方仓库自带的 compose.yaml 采用 goatcounter + PostgreSQL 17 组合;本文采用更简单的单容器 SQLite 形态(与官方二进制默认行为一致),SQLite 已静态编译进二进制,无外部数据库依赖。若日后流量增长需要切 PostgreSQL,改 -db 参数即可,迁移方式见《GoatCounter 2.7.0 配置参数详解》。

三、前置条件

部署开始前需要确认服务器具备以下条件,逐项核验并给出验证命令。

3.1 Docker 与 Compose 插件

$ docker --version && docker compose version
Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1

docker compose version 有正常回显即说明 Compose v2 插件可用;若提示 docker: 'compose' is not a docker command,需先安装 docker-compose-plugin。

3.2 端口规划

本文占用 8141/tcp(宿主机 → 容器 1:1 映射)。选端口时避开服务器上已监听的端口,检查方法:

$ ss -ltn | grep 8141 || echo "8141 空闲"
8141 空闲

部署完成后再次执行 ss -ltn | grep 8141,应看到 0.0.0.0:8141 与 [::]:8141 两条 LISTEN。

3.3 域名与 DNS 规划(重要)

GoatCounter 按请求的 Host 头路由站点:数据库里每个站点绑定一个域名(vhost),请求的 Host 与之匹配才进入对应站点的控制台;登录 Cookie 也以 Domain=<站点域名> 下发。因此:

  1. 若部署在公网服务器:把一个真实域名(如 stats.example.com)的 A/AAAA 记录解析到服务器 IP;
  2. 若仅内网/本机使用:在访问端设备的 hosts 文件里固定映射(如 192.168.1.10 stats.example.com),或建站时直接把 vhost 填成服务器 IP / 127.0.0.1;
  3. 不用域名直接以 IP 访问也能打开登录页,但登录不会生效(Cookie 域不匹配,实测排障见 4.6 与 11.2)。

本文统一以 stats.example.com 为站点域名。

3.4 目录规划

$ mkdir -p /opt/goatcounter/data
路径用途
/opt/goatcounter/Compose 项目目录(compose.yaml 所在)
/opt/goatcounter/data/SQLite 数据库 bind mount 目录(宿主侧持久化)

⚠️ 数据目录属主问题:镜像以 uid 1000(goatcounter)运行,mkdir -p 新建的目录属主是 root 时首次启动会报 DB dir not writeable(完整排障实录见 9.2)。把属主改为 1000 一次性解决:

$ chown -R 1000:1000 /opt/goatcounter/data

四、部署步骤

本章从零开始:编写 compose 文件 → 启动 → 完成首次初始化(创建站点与管理员)→ 验证登录与统计页。所有命令在 /opt/goatcounter/ 下执行。

4.1 编写 compose.yaml

在 /opt/goatcounter/ 下创建 compose.yaml,全文如下(与实测部署完全一致):

name: goatcounter-2.7.0-stack

services:
  goatcounter:
    image: arp242/goatcounter:2.7.0
    command: serve -listen :8141 -public-port 8141 -db sqlite+/home/goatcounter/goatcounter-data/db.sqlite3 -automigrate
    ports:
      - "8141:8141"
    volumes:
      - ./data:/home/goatcounter/goatcounter-data
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "--header", "Host: stats.example.com", "http://127.0.0.1:8141/"]
      interval: 30s
      timeout: 5s
      start_period: 15s
      retries: 3
    restart: unless-stopped
    networks:
      - goatcounter-net

networks:
  goatcounter-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.61.0/24

逐项说明:

配置项值作用
namegoatcounter-2.7.0-stackCompose 项目名。多套 compose 项目共存时防止目录同名互相干扰,docker compose down 只影响本项目容器/网络
commandserve -listen :8141 -public-port 8141 -db sqlite+… -automigrate服务主命令:监听 8141、对外端口 8141、SQLite 数据库文件路径、启动时自动跑数据库迁移。四个参数缺一不可,语义详见七
ports8141:8141宿主机 8141 → 容器 8141。-listen 改端口时这里必须同步改,否则容器内换了端口而宿主机还映射旧端口,访问 502/拒绝连接(排障实录见 11.1)
volumes./data:/home/goatcounter/goatcounter-data把宿主机 /opt/goatcounter/data 挂为容器数据目录,SQLite 库文件落盘宿主机
healthcheckwget 本机 8141 并携带 Host 头探活请求必须带 Host: stats.example.com 模拟真实站点访问,保证探活行为与真实流量一致;GoatCounter 按 Host 路由,探活与业务访问的域名语义保持一致最稳妥
restartunless-stopped服务器重启或进程崩溃后自动拉起(手动 stop 的除外)
networksgoatcounter-net,显式子网 10.89.61.0/24单服务也用自定义网络(默认 bridge 无内置 DNS,多服务扩展时直接可复用);显式固定子网,避免 Compose 自动分配网段与既有网络冲突

⚠️ 项目名规范化实测:Compose 项目名不允许出现点号,name: goatcounter-2.7.0-stack 中的 2.7.0 会被规范化为 270——实际项目名是 goatcounter-270-stack,容器名相应为 goatcounter-270-stack-goatcounter-1(见 4.2 起的所有回显)。这是 Compose 的名称清理规则,不是配置错误;多项目共存时以 docker compose ls 显示的规范化名称为准。

💡 技术拓展:restart 策略不止 unless-stopped 一种——

策略行为适用场景
no(默认)不自动重启一次性调试任务
always总是重启(含 daemon 重启后)无论手动 stop 与否都拉起
unless-stopped崩溃/开机自启,但手动 stop 后保持停止常驻服务标准选择
on-failure[:N]仅异常退出时重启,可限次数需要区分正常退出与异常退出的场景

本文采用 unless-stopped: GoatCounter 是常驻统计服务,需要开机自启与崩溃自愈,同时保留"人工停服维护"的意志。

4.2 启动与首次启动日志

$ cd /opt/goatcounter
$ docker compose up -d
 Network goatcounter-270-stack_goatcounter-net Creating
 Network goatcounter-270-stack_goatcounter-net Created
 Container goatcounter-270-stack-goatcounter-1 Creating
 Container goatcounter-270-stack-goatcounter-1 Created
 Container goatcounter-270-stack-goatcounter-1 Starting
 Container goatcounter-270-stack-goatcounter-1 Started

首次启动(全新数据目录)日志回显如下(docker compose logs --no-color,节选保留关键行):

goatcounter-1  | Nov  1 03:31:55  INFO  startup: GoatCounter ready
goatcounter-1  |                         num_sites         = 0
goatcounter-1  |                         sites             = []
goatcounter-1  |                         listen            = :8141
goatcounter-1  |                         dev               = false
goatcounter-1  |                         version.version   = 7e91d8a9bdbb_2025-12-15T18:20:31Z-dev
goatcounter-1  |                         version.go        = go1.25.5
goatcounter-1  |                         version.GOOS      = linux
goatcounter-1  |                         version.GOARCH    = amd64
goatcounter-1  |                         version.CGO       = true
goatcounter-1  |                         version.race      = false
goatcounter-1  |                         geoip.path        =
goatcounter-1  |                         geoip.build       = 2025-05-20 09:30:47
goatcounter-1  |                         geoip.type        = GeoLite2-Country
goatcounter-1  |                         geoip.description = GeoLite2 Country database
goatcounter-1  |                         geoip.nodes       = 1522374
goatcounter-1  |                         location          = zgo.at/goatcounter/v2/cmd/goatcounter/serve.go:339
goatcounter-1  | Nov  1 03:31:55  WARN  No sites yet; access the web interface or use the CLI to create one:
goatcounter-1  |                             docker exec -it e7e71eda524e goatcounter db -db="sqlite+/home/goatcounter/goatcounter-data/db.sqlite3" create site -vhost=.. -user.email=..
goatcounter-1  |                         location = zgo.at/goatcounter/v2/pkg/log/log.go:109

两个关键信号:startup: GoatCounter ready(服务就绪)与 WARN No sites yet(库里还没有站点,需要初始化——服务本身已可用,等一个站点被创建)。

此时健康检查尚未跑满 start_period,等半分钟后确认容器进入 healthy:

$ docker inspect goatcounter-270-stack-goatcounter-1 --format '{{.State.Health.Status}}'
healthy

4.3 初始化方式一:Web 向导(推荐)

站点数为 0 时访问任意路径都会进入首个初始化向导。以 curl 观察页面结构(真实部署用浏览器访问 http://stats.example.com:8141/,观感一致):

$ curl -s http://127.0.0.1:8141/ | grep -oE '<h1[^>]*>[^<]*</h1>|<form[^>]*>|name="[a-z_2]+"'
<h1>Create your first site and user</h1>
<form method="POST" action="/">
name="email"
name="password"
name="password2"
name="vhost"

向导页表单字段:

字段含义本文取值
email管理员登录邮箱admin@example.com
password / password2登录密码(两次输入)q1w2e3r4
vhost站点域名(页面默认值 127.0.0.1,可改)stats.example.com

提交表单后服务端创建站点与用户并直接种下登录 Cookie(实测回显):

$ curl -s -D - -o /dev/null -X POST http://127.0.0.1:8141/ \
    --data-urlencode 'email=admin@example.com' \
    --data-urlencode 'password=q1w2e3r4' \
    --data-urlencode 'password2=q1w2e3r4' \
    --data-urlencode 'vhost=stats.example.com' | head -6
HTTP/1.1 303 See Other
Location: /
Set-Cookie: key=20261101-1zyb3z7npuu7v1ppkeualyh6ixqf28jegszy71fdqdb6w2wr5q; Path=/; Expires=Mon, 01 Nov 2027 03:33:28 GMT; HttpOnly; SameSite=Lax
Date: Sun, 01 Nov 2026 03:33:28 GMT
Content-Type: text/html; charset=utf-8

HTTP/1.1 303 + Set-Cookie: key=... 表示站点与用户创建成功且已自动登录。浏览器访问 http://stats.example.com:8141/ 时同样的表单提交后直接进入统计控制台。

4.4 初始化方式二:CLI 建站

服务日志里的 WARN 提示给出了 CLI 方式。在容器内执行 goatcounter db create site(-vhost 为站点域名、-user.email 为管理员邮箱,-password 直接指定登录密码,避免交互式输密码):

$ docker compose exec -T goatcounter goatcounter db create site \
    -vhost stats.example.com \
    -user.email admin@example.com \
    -password q1w2e3r4
$ echo "exit=$?"
exit=0

⚠️ 两个实测注意点:

  1. docker compose exec 后面加 -T(关闭伪终端分配)。不加 -T 时命令会尝试交互式读密码并报错(排障实录见 11.3);
  2. 不带 -password 时命令会进入交互式 Enter password for new user (will not echo):,适合真机操作;脚本/非交互场景一律带 -password。

4.5 初始化结果核验

用 goatcounter db query 直接查库确认站点与用户落库(serve 命令自带 -db 参数,容器内 exec 默认数据库路径即挂载卷中的 db.sqlite3):

$ docker exec goatcounter-270-stack-goatcounter-1 \
    goatcounter db query "select site_id, code, cname, state from sites"
site_id  code                cname              state
1        serve-oiduizuimhos  stats.example.com  a

$ docker exec goatcounter-270-stack-goatcounter-1 \
    goatcounter db query "select user_id, email, email_verified from users"
user_id  email              email_verified
1        admin@example.com  1

state = a(active)说明站点已激活;code 是 serve 自动生成的站点代号。

4.6 登录页与统计台验证

站点创建后,未登录访问根路径会 303 跳到登录页(/user/new,页面标题变为 "Sign in at stats.example.com"):

$ curl -s -D - -o /dev/null -H 'Host: stats.example.com' http://127.0.0.1:8141/ | head -6
HTTP/1.1 303 See Other
Cache-Control: no-store,no-cache
Content-Security-Policy: frame-ancestors 'none'; default-src 'none'; font-src 'self'; form-action 'self'; manifest-src 'self'; connect-src 'self' wss:; frame-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'
Location: /user/new
Set-Cookie: flash=iTmVlZCB0byBsb2cgaW4=; Path=/; Expires=Sun, 01 Nov 2026 03:50:50 GMT; HttpOnly; SameSite=Lax
Strict-Transport-Security: max-age=63072000
$ curl -s -L -H 'Host: stats.example.com' http://127.0.0.1:8141/ | grep -oE '<h1[^>]*>[^<]*</h1>'
<h1>Sign in at stats.example.com</h1>

带正确 Host 头提交登录(表单 action 为 /user/requestlogin),服务端返回 303 并种下登录 Cookie:

$ curl -s -D - -o /dev/null -X POST -H 'Host: stats.example.com' http://127.0.0.1:8141/user/requestlogin \
    --data-urlencode 'email=admin@example.com' \
    --data-urlencode 'password=q1w2e3r4' \
    --data-urlencode 'always=true' | grep -E 'HTTP|Set-Cookie|Location'
HTTP/1.1 303 See Other
Location: /
Set-Cookie: key=20261101-1zyb3z7npuu7v1ppkeualyh6ixqf28jegszy71fdqdb6w2wr5q; Path=/; Domain=stats.example.com; Expires=Mon, 01 Nov 2027 03:52:21 GMT; HttpOnly; SameSite=Lax

携带该 Cookie 访问根路径,返回 200 的统计控制台(页面含 Top referrers / Campaigns / Browsers / Systems / Locations / Languages 等统计模块):

$ curl -s -H 'Host: stats.example.com' \
    -b 'key=20261101-1zyb3z7npuu7v1ppkeualyh6ixqf28jegszy71fdqdb6w2wr5q' \
    http://127.0.0.1:8141/ -o /dev/null -w '%{http_code}\n'
200

浏览器侧操作等价:hosts 解析好 stats.example.com 后访问 http://stats.example.com:8141/,输入 admin@example.com / q1w2e3r4 登录即可看到同样的控制台。

⚠️ Host 头语义实测:同样的登录请求若 Host 不是 stats.example.com(例如直接 curl http://127.0.0.1:8141/...),服务端同样返回 303,但不种 Cookie(响应头无 Set-Cookie,因为 Cookie 的 Domain=stats.example.com 与访问域名不符)。表现就是"登录表单提交了、页面刷新还是登录页"。解法:用域名访问(DNS 或 hosts),或建站时 vhost 直接填 127.0.0.1/服务器 IP。详见 11.2。

至此部署完成。日常给业务站点埋点时,把 <script data-goatcounter="https://stats.example.com/count"></script> 等计数代码加到被统计页面即可;纯后端探测/压测的请求会被记录为 bot 而不进统计(实测见 11.4)。

五、数据持久化说明

GoatCounter 的全部状态就是那一个 SQLite 库文件(外加 WAL/SHM 伴生文件),持久化 = 保住这个目录。

5.1 宿主机落盘形态

部署一段时间后,/opt/goatcounter/data/ 下的文件(实测回显):

$ ls -la /opt/goatcounter/data/ | grep sqlite
-rw-r--r-- 1 1000 1000 507904 Nov  1 11:51 db.sqlite3
-rw-r--r-- 1 1000 1000  32768 Nov  1 11:51 db.sqlite3-shm
-rw-r--r-- 1 1000 1000   8272 Nov  1 11:51 db.sqlite3-wal
文件说明
db.sqlite3主库文件:站点、用户、访问记录、聚合统计全在这里
db.sqlite3-wal / -shmSQLite 预写日志与共享内存文件;正常停机后会合并回主文件,运行中存在属正常现象

⚠️ 备份这三个文件一起拷(或先 docker compose stop 再拷),只拷 db.sqlite3 而忽略 -wal 可能丢失最近一段写入。

5.2 重启不丢数据实测

向站点灌入计数请求(/count 端点返回 200),等 GoatCounter 的定期落盘周期(默认 60 秒)后重启容器,数据保留(实测回显,bots 表为 2.7.0 新增的机器人浏览记录表):

$ curl -s -o /dev/null -w 'count=%{http_code}\n' -H 'Host: stats.example.com' \
    'http://127.0.0.1:8141/count?p=/persist-test&t=persist&rnd=1'
count=200
$ docker compose restart
 Container goatcounter-270-stack-goatcounter-1  Restarting
 Container goatcounter-270-stack-goatcounter-1  Started
$ docker exec goatcounter-270-stack-goatcounter-1 \
    goatcounter db query "select count(*) as bots from bots"
bots
2
$ docker exec goatcounter-270-stack-goatcounter-1 \
    goatcounter db query "select user_id, email from users"
user_id  email
1        admin@example.com

重启后记录与用户原样保留,bind mount 生效。

六、环境变量表

镜像与二进制共用一套配置面:所有 serve 旗标都有等价的 GOATCOUNTER_<大写旗标> 环境变量(官方 help 示例:GOATCOUNTER_LISTEN=:80、GOATCOUNTER_STORE_EVERY=60、GOATCOUNTER_AUTOMIGRATE=)。compose 中两种写法等价,环境变量法不用改 command:

    environment:
      GOATCOUNTER_LISTEN: ":8141"
      GOATCOUNTER_PUBLIC_PORT: "8141"
      GOATCOUNTER_DB: "sqlite+/home/goatcounter/goatcounter-data/db.sqlite3"
      GOATCOUNTER_AUTOMIGRATE: ""

常用环境变量(本文口径):

环境变量等价旗标默认值说明
GOATCOUNTER_LISTEN-listen*:8080监听地址与端口
GOATCOUNTER_PUBLIC_PORT-public-port(80/443 时无需设置)对外访问端口,写入重定向/邮件里的链接
GOATCOUNTER_DB-dbsqlite+./goatcounter-data/db.sqlite3数据库连接串
GOATCOUNTER_AUTOMIGRATE-automigrate关闭设为任意非空值即启动时自动跑迁移
GOATCOUNTER_TMPDIRTMPDIR系统临时目录CSV 导出临时文件目录(v2.7.0 新增该别名)
GOATCOUNTER_SMTP-smtpstdoutSMTP 发信串;默认 stdout 表示邮件打印到日志而不外发
GOATCOUNTER_EMAIL_FROM-email-from<user>@<hostname>发件人地址

⚠️ 环境变量与 command 旗标混用时,旗标优先级更高。本文统一只写 command,避免两处配置漂移。

七、配置参数说明

compose 中 command: 一行决定 GoatCounter 的运行参数。逐个旗标说明(依据 goatcounter help serve 原文与实测行为):

7.1 -listen 与端口映射

-listen      Address to listen on. Default: "*:8080".
  • 默认监听 8080,本文改为 :8141(监听容器内所有地址的 8141 端口);
  • 改了 -listen 就必须同步改 compose 的 ports 映射与 -public-port,三处一致是排障高频点(实录见 11.1)。

7.2 -public-port 与域名语义

-public-port Port your site is publicly accessible on. Only needed if it's not 80 or 443.

GoatCounter 生成的重定向、密码重置邮件链接需要知道"用户从哪个端口访问"。走 80/443 时它猜得出;走非标准端口(本文 8141)时必须显式声明,否则邮件里的链接会缺端口。本文 -listen :8141 -public-port 8141 成对出现。

7.3 -tls 与域名 TLS 语义

-tls  Serve over tls. 逗号分隔的组合,可选项:
      http              不做 TLS(默认)
      path/to/file.pem  单文件证书+私钥
      acme[:cache]      ACME(Let's Encrypt)自动签发,cache 为证书缓存目录
      rdr              把 80 端口重定向到 -listen 端口

域名与 TLS 的组合语义:

场景command 写法说明
HTTP 直连(本文)不写 -tls(默认 http)http://stats.example.com:8141;Cookie 的 secure 属性自动按连接类型判断(v2.7.0 起)
ACME 自动证书-listen :443 -tls tls,rdr,acme80 必须可从公网访问(Let's Encrypt 验证需要),80 自动 301 到 443;证书缓存目录默认 ./goatcounter-data/acme-secrets(随数据卷持久化)
自备证书-listen :443 -tls tls,/etc/tls/stats.example.com.pempem 文件需挂载进容器
前置反代-listen localhost:8081 -tls none(宿主机安装形态)TLS 交给 nginx/Caddy;容器形态下反代容器与 goatcounter 同网络,-listen 写服务名

💡 技术拓展:TLS 证书获取路径不止 ACME 一种——

方式特点适用场景
acme(内建)全自动签发续期,要求 80 端口公网可达公网服务器直连部署
file.pem(自备)证书自理(CA 签发或自签)内网、已有企业 CA
反代 TLSTLS 在 nginx/Caddy 层终结已有统一网关/证书管理的环境

本文为最小可复现形态采用默认 http;上公网时首选内建 acme。

7.4 -db 与 -automigrate

-db          Database connection: "sqlite+<file>" or "postgres+<connect>". Default: sqlite+./goatcounter-data/db.sqlite3
-automigrate Automatically run all pending migrations on startup.
  • -db sqlite+/home/goatcounter/goatcounter-data/db.sqlite3:库文件路径落在挂载卷内,容器重建数据不丢;
  • -automigrate:启动时自动跑未执行的库结构迁移。升级版本时该参数让新二进制自动完成库升级(实测升级实录见 10.2);不写该参数时升级后首启会报迁移提示,需要手动 goatcounter db migrate all。

7.5 邮件相关(-smtp / -email-from)

-smtp        SMTP relay server, as URL (e.g. "smtp://user:pass@server:port"). Default: "stdout"
-email-from  From: address in emails. Default: <user>@<hostname>

默认 -smtp stdout 时,密码重置等邮件只打印到容器日志而不真正外发——单人使用无需配邮件;多用户部署时配置真实 SMTP 中继(支持 smtps://,v2.7.0 起)。

八、配置文件挂载

GoatCounter 没有配置文件——全部配置就是 command 旗标与环境变量,因此本部署不存在"挂载配置文件"一项,只有数据目录一个挂载:

挂载方向作用
./data → /home/goatcounter/goatcounter-databind mountSQLite 库文件 + ACME 证书缓存(若启用)的宿主机持久化位置

如需 ACME 证书缓存持久化,无需新增挂载:acme 缓存目录默认就在 ./goatcounter-data/acme-secrets,已随数据目录落盘宿主机。

若日后想固定一份"配置"以便审查,把 command 里的参数写进 compose.yaml 本身(如本文)就是配置文件;或改用 GOATCOUNTER_ 环境变量统一在 environment: 段管理(见六)。

九、等价 docker run

9.1 等价命令

不使用 compose 时,下面一条 docker run 与本文 compose 完全等价(网络用默认 bridge,单服务场景不影响功能):

$ docker run -d --name goatcounter-270 \
    -p 8141:8141 \
    -v /opt/goatcounter/data:/home/goatcounter/goatcounter-data \
    arp242/goatcounter:2.7.0 \
    serve -listen :8141 -public-port 8141 -db sqlite+/home/goatcounter/goatcounter-data/db.sqlite3 -automigrate
cd7f076dd8416779bc1673e97ea7a300d15d68321d9a6630c82253eeb7e62097

参数与 compose 的对应关系:-p 8141:8141 ↔ ports;-v ↔ volumes;镜像后追加的命令串 ↔ command;--name ↔ 容器名。启动后同样 curl http://127.0.0.1:8141/ 应返回初始化向导或登录页(实测 root 路径返回 200)。

9.2 数据目录权限排障实录(首次 run 失败)

上述命令在目录属主为 root 时会启动即退出,实测报错:

$ docker logs goatcounter-270
goatcounter: zdb.Connect: sqlite3.Connect: DB dir not writeable: open /home/goatcounter/goatcounter-data/zdb-write-test-3518947457: permission denied

定位:镜像以非 root 的 goatcounter 用户(uid=1000)运行,启动时先做一次数据目录可写性探测(写 zdb-write-test-* 探针文件),mkdir -p 建出的 root 属主目录写不进去。

解决与验证:

$ chown -R 1000:1000 /opt/goatcounter/data
$ docker start goatcounter-270
goatcounter-270
$ docker ps --filter name=goatcounter-270 --format '{{.Names}} {{.Status}}'
goatcounter-270 Up 5 seconds
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8141/
200

compose 形态同理:只要 ./data 属主是 uid 1000,docker compose up 一次成功。权限报错的修复动作就是这一个 chown。

十、常用运维命令

10.1 日常巡检

$ cd /opt/goatcounter
$ docker compose ps
NAME                                   IMAGE                      COMMAND                 SERVICE      CREATED         STATUS                   PORTS
goatcounter-270-stack-goatcounter-1   arp242/goatcounter:2.7.0   "goatcounter serve -…"   goatcounter   2 minutes ago   Up 2 minutes (healthy)   80/tcp, 443/tcp, 8080/tcp, 0.0.0.0:8141->8141/tcp, [::]:8141->8141/tcp

STATUS 列的 (healthy) 即 healthcheck 通过。看实时日志:

$ docker compose logs -f --no-color
goatcounter-1  | Nov  1 03:31:55  INFO  startup: GoatCounter ready
goatcounter-1  |                         num_sites         = 0
goatcounter-1  |                         listen            = :8141

(输出为实时跟踪模式,内容同 4.2 的启动日志,Ctrl-C 退出;日常巡检重点看有无 ERROR/WARN 级新日志。)

单机 compose 项目清单确认(多组件服务器上防止看错项目):

$ docker compose ls
NAME                        STATUS              CONFIG FILES
goatcounter-270-stack       running(1)          /opt/goatcounter/compose.yaml

停服与移除:

$ docker compose stop      # 停容器,保留容器与网络
$ docker compose down      # 移除容器与网络,数据目录 ./data 不受影响

10.2 版本升级与回滚(实测演练)

升级 = 换镜像 tag + 重建容器,-automigrate 自动完成库结构迁移。以下为 2.6.0 → 2.7.0 的完整升级实录(升级前先备份数据目录)。实录在升级演练实例上执行:同一台服务器上用与 4.1 相同结构的 compose 跑了一个 2.6.0 演练栈(监听 8144),逐步升级到 2.7.0 验证迁移路径;对生产 8141 部署操作时把端口换回 8141 即可,步骤完全一致:

$ cp -a /opt/goatcounter/data /opt/goatcounter/data.bak-pre-upgrade

把 compose.yaml 中 image: arp242/goatcounter:2.7.0 改为目标版本(演练为 2.6.0 → 2.7.0 的反向重建后按同流程执行),然后拉取并重建:

$ docker compose pull
 Image arp242/goatcounter:2.7.0 Pulling
 Image arp242/goatcounter:2.7.0 Pulled
$ docker compose up -d

启动日志中出现 2.6 → 2.7 的自动迁移过程(实测回显,迁移按库内版本逐条执行):

goatcounter-1  | Nov  1 03:38:40  INFO  running migration "2025-06-20-1-empty-counts"
goatcounter-1  | Nov  1 03:38:40  INFO  running migration "2025-06-21-1-bots"
goatcounter-1  | Nov  1 03:38:40  INFO  running migration "2025-06-21-2-drop-sizes"
goatcounter-1  | Nov  1 03:38:40  INFO  running migration "2025-07-01-1-share-api-tokens"
goatcounter-1  | Nov  1 03:38:40  INFO  running migration "2025-12-03-1-unique-browsers"
goatcounter-1  | Nov  1 03:38:40  INFO  running migration "2025-12-10-1-datepicker"
goatcounter-1  | Nov  1 03:38:40  INFO  running migration "2025-12-12-1-ref-scheme"

升级后核验:站点、用户与登录能力完整保留(实测):

$ docker compose exec -T goatcounter goatcounter db query "select site_id, cname, state from sites"
site_id  cname              state
1        stats.example.com  a
$ curl -s -D - -o /dev/null -X POST -H 'Host: stats.example.com' http://127.0.0.1:8144/user/requestlogin \
    --data-urlencode 'email=admin@example.com' --data-urlencode 'password=q1w2e3r4' --data-urlencode 'always=true' \
    | grep -cE 'Set-Cookie: key='
1

回滚:库结构迁移不可自动逆向,回滚 = 停服 → 用升级前备份恢复数据目录 → 镜像 tag 改回旧版本 → docker compose up -d:

$ docker compose down
$ rm -rf /opt/goatcounter/data
$ mv /opt/goatcounter/data.bak-pre-upgrade /opt/goatcounter/data
$ # compose.yaml 镜像 tag 改回 2.6.0
$ docker compose up -d

因此升级前 cp -a data 备份是硬前置动作。

十一、常见问题(FAQ)

11.1 端口改了访问不通:8080 与 8141 的映射错位

现象:镜像 EXPOSE 的是 8080,ports 写了 "8141:8080" 后启动正常,但 curl http://127.0.0.1:8141/ 拒绝连接或健康检查失败。

定位:EXPOSE 只是镜像声明,实际监听端口由 -listen 决定。默认 command(serve -automigrate)监听 8080 时 "8141:8080" 能通;一旦把 -listen 改成 :8141,容器内就没有 8080 了,映射宿主 8141 → 容器 8080 自然不通。

解决:三处一致——command 的 -listen :8141、ports 的 "8141:8141"、-public-port 8141。验证:docker compose ps 的 PORTS 列应为 0.0.0.0:8141->8141/tcp,宿主机 ss -ltn | grep 8141 应有 LISTEN。

现象:以 http://127.0.0.1:8141/ 或 http://<服务器IP>:8141/ 访问,登录页正常显示(Sign in at stats.example.com),输入正确账密提交后页面回到登录页,无报错。

定位:登录成功时 Cookie 以 Domain=stats.example.com 下发(4.6 实测回显)。访问域名与 Cookie 域不一致时浏览器拒收,会话建立不起来。用 curl 复现可直接看到差异:正确 Host 的登录响应带 Set-Cookie: key=...; Domain=stats.example.com,而错误 Host 的登录响应只有 HTTP/1.1 303 没有 Set-Cookie。

解决(三选一):

  1. 公网部署:域名解析到服务器,用域名访问(首选);
  2. 内网部署:访问端 hosts 加 <服务器IP> stats.example.com;
  3. 纯本机使用:初始化向导的 vhost 字段保持默认 127.0.0.1(或 CLI -vhost 127.0.0.1),站点域名与访问域名自然一致。

11.3 CLI 建站报 getTermios 错误

现象:docker compose exec goatcounter goatcounter db create site ...(不带 -password)报错:

Enter password for new user (will not echo): goatcounter: getTermios: inappropriate ioctl for device
Command failed with exit code 1

定位:命令默认交互式读密码(关闭回显需要终端设备),docker compose exec 管道场景没有分配 TTY。

解决:加 -T 关闭 TTY 分配并用 -password 直接传密码(见 4.4),或保留交互模式时用 docker compose exec goatcounter ...(默认带 TTY 的手工场景)。

11.4 压测/探测请求不进统计:进了 bots 表

现象:curl http://stats.example.com:8141/count?p=/test 返回 200,但控制台与 hits 表里查不到该访问。

定位:GoatCounter 2.7.0 把判定为机器人的浏览记录单独存入 bots 表(保留 30 天,v2.7.0 Release Notes:"Store bot pageviews in a new bots table for 30 days")。无浏览器 JS 指纹的请求(curl/压测工具/拨测)会被归入 bot,实测:

$ docker exec goatcounter-270-stack-goatcounter-1 \
    goatcounter db query "select path, count(*) as n from bots group by path"
path            n
/persist-check  1
/persist-test   1

结论:这不是丢数据。真实浏览器(执行 count.js 的访问)才计入统计;验证部署链路时以 bots 表或浏览器实际访问为准。

11.5 首启日志报 No sites yet 是故障吗

不是。全新库首启的正常提示(4.2 实测回显),意思是"服务已就绪、还没有站点"。完成 4.3/4.4 任一初始化后提示不再出现;goatcounter db query "select count(*) from sites" 返回 1 即完成初始化。

11.6 升级新版本后启动报迁移提示

不带 -automigrate 时,新版二进制遇到旧库结构会在启动日志提示需要迁移。本文 command 已带 -automigrate 自动完成;手动场景执行 goatcounter db migrate all(详见《GoatCounter 2.7.0 常用 CLI 命令与工具详解》)。升级完整流程与回滚见 10.2。

十二、参考链接