GoatCounter 2.7.0 Docker Compose 部署指南
在 Docker Compose 上部署 GoatCounter 2.7.0 的完整实测指南:compose 全文逐项说明、SQLite 数据持久化、Web 向导与 CLI 双路首次初始化、Host 域名校验与端口映射排障实录、等价 docker run 命令及 2.6→2.7 升级回滚演练。
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 Engine | 29.9.2 | 含 Compose v2 插件(docker compose 子命令) |
| Docker Compose | v5.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.com | GoatCounter 按域名路由站点,本文全程用该示例域名 |
| 数据目录 | /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/8080 | 8080 是镜像内默认监听端口(-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=<站点域名> 下发。因此:
- 若部署在公网服务器:把一个真实域名(如
stats.example.com)的 A/AAAA 记录解析到服务器 IP; - 若仅内网/本机使用:在访问端设备的 hosts 文件里固定映射(如
192.168.1.10 stats.example.com),或建站时直接把 vhost 填成服务器 IP /127.0.0.1; - 不用域名直接以 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
逐项说明:
| 配置项 | 值 | 作用 |
|---|---|---|
| name | goatcounter-2.7.0-stack | Compose 项目名。多套 compose 项目共存时防止目录同名互相干扰,docker compose down 只影响本项目容器/网络 |
| command | serve -listen :8141 -public-port 8141 -db sqlite+… -automigrate | 服务主命令:监听 8141、对外端口 8141、SQLite 数据库文件路径、启动时自动跑数据库迁移。四个参数缺一不可,语义详见七 |
| ports | 8141:8141 | 宿主机 8141 → 容器 8141。-listen 改端口时这里必须同步改,否则容器内换了端口而宿主机还映射旧端口,访问 502/拒绝连接(排障实录见 11.1) |
| volumes | ./data:/home/goatcounter/goatcounter-data | 把宿主机 /opt/goatcounter/data 挂为容器数据目录,SQLite 库文件落盘宿主机 |
| healthcheck | wget 本机 8141 并携带 Host 头 | 探活请求必须带 Host: stats.example.com 模拟真实站点访问,保证探活行为与真实流量一致;GoatCounter 按 Host 路由,探活与业务访问的域名语义保持一致最稳妥 |
| restart | unless-stopped | 服务器重启或进程崩溃后自动拉起(手动 stop 的除外) |
| networks | goatcounter-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"
向导页表单字段:
| 字段 | 含义 | 本文取值 |
|---|---|---|
| 管理员登录邮箱 | 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
⚠️ 两个实测注意点:
docker compose exec后面加-T(关闭伪终端分配)。不加-T时命令会尝试交互式读密码并报错(排障实录见 11.3);- 不带
-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 / -shm | SQLite 预写日志与共享内存文件;正常停机后会合并回主文件,运行中存在属正常现象 |
⚠️ 备份这三个文件一起拷(或先
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 | -db | sqlite+./goatcounter-data/db.sqlite3 | 数据库连接串 |
| GOATCOUNTER_AUTOMIGRATE | -automigrate | 关闭 | 设为任意非空值即启动时自动跑迁移 |
| GOATCOUNTER_TMPDIR | TMPDIR | 系统临时目录 | CSV 导出临时文件目录(v2.7.0 新增该别名) |
| GOATCOUNTER_SMTP | -smtp | stdout | SMTP 发信串;默认 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,acme | 80 必须可从公网访问(Let's Encrypt 验证需要),80 自动 301 到 443;证书缓存目录默认 ./goatcounter-data/acme-secrets(随数据卷持久化) |
| 自备证书 | -listen :443 -tls tls,/etc/tls/stats.example.com.pem | pem 文件需挂载进容器 |
| 前置反代 | -listen localhost:8081 -tls none(宿主机安装形态) | TLS 交给 nginx/Caddy;容器形态下反代容器与 goatcounter 同网络,-listen 写服务名 |
💡 技术拓展:TLS 证书获取路径不止 ACME 一种——
方式 特点 适用场景 acme(内建) 全自动签发续期,要求 80 端口公网可达 公网服务器直连部署 file.pem(自备) 证书自理(CA 签发或自签) 内网、已有企业 CA 反代 TLS TLS 在 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-data | bind mount | SQLite 库文件 + 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。
11.2 能打开登录页但登录不生效:Host 域名与 Cookie 作用域
现象:以 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。
解决(三选一):
- 公网部署:域名解析到服务器,用域名访问(首选);
- 内网部署:访问端 hosts 加
<服务器IP> stats.example.com; - 纯本机使用:初始化向导的 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。
十二、参考链接
- 官方仓库:https://github.com/arp242/goatcounter
- 官方 Releases(v2.7.0,2025-12-15 发布,2026-11-01 复核):https://github.com/arp242/goatcounter/releases
- 官方 Docker Hub:https://hub.docker.com/r/arp242/goatcounter
- 官方网站与文档:https://www.goatcounter.com
- 站内系列文档:《GoatCounter 介绍与选型指南》(定位与同类对比)、《Ubuntu 24.04 安装 GoatCounter 2.7.0 指南》(systemd 裸机安装)、《GoatCounter 2.7.0 配置参数详解》(serve 全量旗标与环境变量)、《GoatCounter 2.7.0 常用 CLI 命令与工具详解》(db/import/dashboard 等子命令)