当一台 VPS 同时运行网站、数据库、反向代理和多个 Docker 服务时,只靠 docker logs 临时翻找日志,很难快速回答三个关键问题:故障从什么时候开始、影响了哪些容器、同类错误是否正在持续增加。
这篇教程会在一台 Ubuntu VPS 上部署一套可长期使用的轻量日志平台:
- Grafana Loki 负责存储与查询日志;
- Grafana 提供搜索、可视化和告警界面;
- Grafana Alloy 自动发现 Docker 容器并采集日志;
- Caddy 只对外暴露 Grafana,并自动配置 HTTPS;
- Loki Compactor 保留最近 30 天日志;
- 离线归档脚本覆盖配置、数据、校验和与恢复演练。
本文固定使用 Loki 3.7.6、Grafana 13.2.1 和 Alloy 1.19.2。它们是撰写时各自对应分支的稳定版本;实际部署前仍应检查发行说明,尤其不要跨大版本盲目升级。
Loki 不为日志内容建立全文索引,而是主要索引标签,因此在相同规模下通常比 Elasticsearch 或 OpenSearch 更节省资源。它很适合以下场景:
- 一台或少量 VPS 上运行多个 Docker Compose 项目;
- 需要按容器、Compose 服务或主机查询日志;
- 希望为 5xx、登录失败、任务异常等模式设置告警;
- 不想为了每天几百 MB 日志维护一个较重的搜索集群;
- 已经使用 Prometheus、Grafana,想统一监控入口。
如果你的核心需求是复杂全文搜索、日志字段分析或安全事件调查,OpenSearch 可能更合适;如果主要关心应用异常堆栈、版本与用户影响范围,可以先看 VPS 自建 Sentry 教程。而仅用 docker logs 的优点是零部署,但容器重建、跨服务关联、历史保留和告警都比较弱。
Grafana 官方把单体模式定位为每天大约 20 GB 以内的日志规模。对普通单机 VPS,更应该把它理解为架构上限,而不是容量目标:CPU、内存、磁盘 IOPS 和查询跨度往往会更早成为瓶颈。
下面是保守的起步规格:
| 日志量 | 建议配置 | 说明 |
|---|---|---|
| 少于 500 MB/天 | 2 vCPU、4 GB 内存、40 GB SSD | 适合少量网站和后台任务 |
| 500 MB–2 GB/天 | 4 vCPU、8 GB 内存、100 GB SSD | 适合多个业务容器 |
| 2–5 GB/天 | 6–8 vCPU、16 GB 内存、200 GB NVMe | 需要更严格控制查询与标签 |
磁盘不要只按原始日志量计算。以每天 1 GB、保留 30 天为例,至少要为 Loki 数据、索引、压缩过程、Docker 自身日志和备份临时空间预留 60–100 GB。文件系统对象存储不会根据剩余空间自动删除数据,必须同时配置保留策略和磁盘告警。
网络流量通常不是主要成本,因为 Alloy、Loki 与 Grafana 都在同一台 VPS 的 Docker 网络内通信。真正可能产生外网流量的是浏览器查询大时间范围、远程备份以及把其他主机的日志推送到这台 Loki。跨主机采集时,应把入口放在 HTTPS 和认证之后,并估算每天日志量与出口计费。
本教程的请求链路如下:
Docker 容器 stdout/stderr
|
v
Grafana Alloy --> Loki :3100(仅 Docker 内网)
|
v
Grafana :3000(仅 127.0.0.1)
|
v
Caddy HTTPS :443
Loki 不内置身份认证,所以不要把 3100 端口直接开放到公网。Grafana 也只绑定本机回环地址,由 Caddy 统一处理公网 TLS。
先把一个子域名,例如 logs.example.com,解析到 VPS 公网 IP。然后更新系统并安装 Docker、Caddy:
sudo apt update
sudo apt install -y ca-certificates curl caddy
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
重新登录 SSH 使 Docker 用户组生效。如果服务器上还没有基础加固,至少要禁用密码 SSH、启用密钥登录并限制管理端口来源。
只开放 SSH、HTTP 和 HTTPS。使用 UFW 时:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
不要开放 3000 和 3100。后面的 Compose 文件还会把 Grafana 限制到 127.0.0.1,形成第二层保护。
sudo mkdir -p /opt/loki-stack/{loki,grafana/provisioning/datasources,alloy}
sudo chown -R "$USER":"$USER" /opt/loki-stack
cd /opt/loki-stack
openssl rand -base64 30
把生成的随机值保存到密码管理器,然后创建 .env:
umask 077
printf 'GRAFANA_ADMIN_USER=admin\nGRAFANA_ADMIN_PASSWORD=替换为刚才生成的密码\n' > .env
确认权限:
chmod 600 .env
ls -l .env
创建 /opt/loki-stack/loki/loki.yaml:
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 720h
ingestion_rate_mb: 8
ingestion_burst_size_mb: 16
max_query_length: 744h
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
analytics:
reporting_enabled: false
这里使用 TSDB、v13 schema 和 24 小时索引周期。retention_period: 720h 表示保留 30 天;Loki 的保留默认并不会自动启用,所以还必须设置 compactor.retention_enabled: true。Compactor 的工作目录要放在持久卷中,否则重建容器后可能丢失待删除标记。
如果磁盘较小,可先改为 336h(14 天)。不要把删除文件当作应急清盘方式,先缩短保留时间并让 Compactor 正常执行。
创建 /opt/loki-stack/grafana/provisioning/datasources/loki.yaml:
apiVersion: 1
datasources:
- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
editable: false
jsonData:
maxLines: 1000
http://loki:3100 是 Compose 内部服务地址。不要填 localhost:3100,因为 Grafana 容器里的 localhost 指向 Grafana 自己。
创建 /opt/loki-stack/alloy/config.alloy:
discovery.docker "local" {
host = "unix:///var/run/docker.sock"
refresh_interval = "5s"
}
discovery.relabel "docker_logs" {
targets = discovery.docker.local.targets
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
target_label = "service_name"
}
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
target_label = "compose_project"
}
}
loki.source.docker "local" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.local.targets
relabel_rules = discovery.relabel.docker_logs.rules
labels = {
host = "vps-01",
job = "docker",
}
forward_to = [loki.write.local.receiver]
}
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
这段配置为日志保留 container、service_name、compose_project、host 和 job 标签。不要把请求 ID、用户 ID、URL 完整路径或时间戳做成标签,否则标签基数会快速膨胀。
Alloy 需要读取 Docker API,因此会挂载 /var/run/docker.sock。即使挂载参数写了 :ro,它也不等于 API 层面的只读权限;能访问 Docker socket 的进程通常具有接近主机 root 的能力。只运行官方镜像、固定版本、限制配置文件写权限,并让这台主机只承担可信工作负载。
创建 /opt/loki-stack/compose.yaml:
services:
loki:
image: grafana/loki:3.7.6
command: -config.file=/etc/loki/loki.yaml
restart: unless-stopped
ports:
- "127.0.0.1:3100:3100"
volumes:
- ./loki/loki.yaml:/etc/loki/loki.yaml:ro
- loki-data:/loki
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3100/ready"]
interval: 15s
timeout: 5s
retries: 10
logging:
options:
max-size: "10m"
max-file: "3"
grafana:
image: grafana/grafana:13.2.1
restart: unless-stopped
depends_on:
loki:
condition: service_healthy
ports:
- "127.0.0.1:3000:3000"
environment:
GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER}"
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD}"
GF_USERS_ALLOW_SIGN_UP: "false"
GF_AUTH_ANONYMOUS_ENABLED: "false"
GF_SERVER_ROOT_URL: "https://logs.example.com"
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
logging:
options:
max-size: "10m"
max-file: "3"
alloy:
image: grafana/alloy:v1.19.2
command:
- run
- --storage.path=/var/lib/alloy/data
- /etc/alloy/config.alloy
restart: unless-stopped
depends_on:
loki:
condition: service_healthy
volumes:
- ./alloy/config.alloy:/etc/alloy/config.alloy:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- alloy-data:/var/lib/alloy/data
logging:
options:
max-size: "10m"
max-file: "3"
volumes:
loki-data:
grafana-data:
alloy-data:
Compose 中的 logging 限制非常重要:Alloy 会采集其他容器的日志,但它自己、Loki 和 Grafana 的 stdout 仍由 Docker 保存。如果完全不轮转,这部分日志也可能占满根分区。关于资源限制与健康检查,可参考 Docker Compose 生产配置教程。
先做静态检查:
cd /opt/loki-stack
docker compose config --quiet
docker run --rm \
-v "$PWD/loki/loki.yaml:/etc/loki/loki.yaml:ro" \
grafana/loki:3.7.6 \
-config.file=/etc/loki/loki.yaml -verify-config
docker run --rm \
-v "$PWD/alloy/config.alloy:/etc/alloy/config.alloy:ro" \
grafana/alloy:v1.19.2 \
fmt --check /etc/alloy/config.alloy
配置检查通过后启动:
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 loki alloy grafana
curl -fsS http://127.0.0.1:3100/ready
curl -fsS http://127.0.0.1:3000/api/health
Loki 就绪接口应返回 ready,Grafana 健康接口应返回数据库状态为 ok。
编辑 /etc/caddy/Caddyfile:
logs.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
检查并加载:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://logs.example.com/login
Caddy 会自动申请和续期证书。更完整的反向代理排错方法见 VPS Caddy 自动 HTTPS 教程。
首次登录后,立即创建一个非默认管理员账户,验证可用后禁用或重命名默认 admin。若多人使用,建议接入支持 MFA 的身份提供商,而不是共享一个管理员密码。
运行一个会输出测试日志的临时容器:
docker run --rm alpine:3.22 sh -c \
'echo "loki-smoke-test level=info"; echo "sample failure level=error" >&2'
等待十几秒,在 Grafana 中进入 Drilldown > Logs 或 Explore,选择 Loki 数据源,查询:
{job="docker"} |= "loki-smoke-test"
常用查询示例:
{compose_project="myapp"}
{service_name="nginx"} |~ "(?i)error|critical|panic"
sum by (service_name) (
count_over_time({job="docker"} |~ "(?i)error|exception" [5m])
)
topk(10, sum by (service_name) (
rate({job="docker"}[5m])
))
查询时先用低基数标签缩小范围,再用 |= 或 |~ 过滤内容。不要默认查询全部容器的 30 天日志;这会制造高内存占用和很长的响应时间。
Loki 的成本很大程度取决于日志流数量。适合做标签的字段通常只有少数稳定集合:
- 主机名;
- 环境,如 production、staging;
- Compose 项目;
- 服务名;
- 容器名。
不适合做标签的字段包括 IP、订单号、用户 ID、请求 ID、完整 URL、随机任务 ID。它们应该保留在日志正文中,在查询阶段解析或过滤。
如果发现日志流数量持续上升,先检查应用是否把动态值写进 Docker 标签,再查看 Alloy 的重标记规则。为每项业务添加标签之前,都问一句:这个值的可能取值是否有限且稳定?
Grafana 管理的告警规则可以直接使用 Loki 查询。创建一个规则,表达式例如:
sum by (service_name) (
count_over_time({job="docker"} |~ "(?i)error|exception|panic" [5m])
) > 20
把 Pending period 设为 5 分钟,避免部署时短暂错误立刻触发。通知策略可以接入邮件、Slack、Telegram 或 Webhook。
不要只设置“出现 error 就告警”。不同服务的正常错误基线差异很大,建议观察一周后按服务设置阈值,并配合 Prometheus 与 Grafana 监控教程 的 CPU、内存、磁盘和服务存活指标判断影响范围。
还应建立至少三类平台自身告警:
- 根分区或 Docker 数据盘使用率超过 80%;
- Loki 容器重启或
/ready失败; - Alloy 丢弃日志、写入失败或持续重连。
日志平台会集中保存多个应用的数据,风险可能高于单个业务服务。部署时至少遵守以下规则:
- 应用不要记录密码、Cookie、Authorization 头、API Key 或完整支付信息;
- 在应用层优先脱敏,采集端正则删除只能作为补充;
- 不向公网开放 Loki 3100,也不开放 Alloy 调试界面;
- 限制 Grafana 管理员数量,普通查看者使用 Viewer 权限;
- 定期检查数据源代理和插件权限;
- 备份文件加密后再传到远端。
Loki 的 auth_enabled: false 只适用于这个封闭的单机 Docker 网络。如果要接收其他 VPS 的日志,应在 Loki 前增加带认证的反向代理或使用支持租户认证的网关,不能直接把 3100 暴露出去。
单机文件系统模式最容易理解的备份方式,是短暂停写后归档三个数据卷和配置文件。创建 /usr/local/sbin/backup-loki-stack.sh:
#!/usr/bin/env bash
set -Eeuo pipefail
STACK_DIR="/opt/loki-stack"
BACKUP_ROOT="/var/backups/loki-stack"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
TARGET="$BACKUP_ROOT/loki-stack-$STAMP.tar.gz"
mkdir -p "$BACKUP_ROOT"
cd "$STACK_DIR"
docker compose stop alloy grafana loki
trap 'docker compose start loki grafana alloy' EXIT
LOKI_VOL="$(docker inspect loki-stack-loki-1 \
--format '{{range .Mounts}}{{if eq .Destination "/loki"}}{{.Name}}{{end}}{{end}}')"
GRAFANA_VOL="$(docker inspect loki-stack-grafana-1 \
--format '{{range .Mounts}}{{if eq .Destination "/var/lib/grafana"}}{{.Name}}{{end}}{{end}}')"
ALLOY_VOL="$(docker inspect loki-stack-alloy-1 \
--format '{{range .Mounts}}{{if eq .Destination "/var/lib/alloy/data"}}{{.Name}}{{end}}{{end}}')"
test -n "$LOKI_VOL"
test -n "$GRAFANA_VOL"
test -n "$ALLOY_VOL"
docker run --rm \
-v "$LOKI_VOL:/source/loki:ro" \
-v "$GRAFANA_VOL:/source/grafana:ro" \
-v "$ALLOY_VOL:/source/alloy:ro" \
-v "$STACK_DIR:/source/config:ro" \
-v /etc/caddy:/source/caddy:ro \
-v "$BACKUP_ROOT:/backup" \
alpine:3.22 \
tar -czf "/backup/$(basename "$TARGET")" -C /source \
loki grafana alloy config caddy
sha256sum "$TARGET" > "$TARGET.sha256"
find "$BACKUP_ROOT" -type f -mtime +14 -delete
授权并手动执行一次:
sudo chmod 700 /usr/local/sbin/backup-loki-stack.sh
sudo /usr/local/sbin/backup-loki-stack.sh
sudo ls -lh /var/backups/loki-stack
sudo sha256sum -c /var/backups/loki-stack/*.sha256
脚本假设 Compose 项目名为 loki-stack。如果目录名不同,容器名也会不同,应先运行 docker compose ps 和 docker inspect 确认。生产环境可改用卷标签或容器 ID,避免依赖生成名称。
离线归档会造成短暂停写,但能得到一致的数据快照。若日志不能中断,应改用对象存储和相应备份策略,而不是在活跃文件上直接 tar。远程备份与恢复演练可继续参考 VPS 备份与恢复演练教程。
不要等故障发生后才第一次尝试恢复。建议在另一台临时 VPS 上完成以下流程:
- 安装相同主版本的 Docker 和 Caddy;
- 校验归档 SHA-256;
- 解压配置和数据到临时目录;
- 创建三个新卷,将对应目录复制进去;
- 检查
compose.yaml中卷名与域名; - 启动 Loki、Grafana、Alloy;
- 查询备份前的一条已知日志;
- 检查 Grafana 数据源、告警规则和用户是否存在。
示例校验与解压:
sha256sum -c loki-stack-20260906T020000Z.tar.gz.sha256
mkdir -p /tmp/loki-restore
tar -xzf loki-stack-20260906T020000Z.tar.gz -C /tmp/loki-restore
find /tmp/loki-restore -maxdepth 2 -type d -print
恢复成功的标准不是“容器能启动”,而是能够查询历史日志、登录 Grafana、读取告警规则,并确认新的日志继续写入。
Loki、Grafana 和 Alloy 应分别升级,不要一次改三个版本。以 Grafana 小版本升级为例:
cd /opt/loki-stack
sudo /usr/local/sbin/backup-loki-stack.sh
docker compose pull grafana
docker compose up -d grafana
docker compose ps
docker compose logs --tail=100 grafana
curl -fsS http://127.0.0.1:3000/api/health
升级前阅读发行说明和破坏性变更,先备份,再修改一个镜像标签。验证登录、数据源、查询和告警后,再升级下一个组件。
回滚时不要只把镜像标签改回去。如果新版本已经迁移了 Grafana 数据库或 Loki 存储格式,应停止服务并恢复升级前快照。完整的变更顺序是:备份、拉取、单组件升级、健康检查、业务查询、观察、再继续。
先从 Grafana 容器内确认服务名解析和网络:
docker compose exec grafana \
wget -qO- http://loki:3100/ready
docker compose logs --tail=200 loki grafana
数据源地址必须是 http://loki:3100,不是 localhost。
docker compose logs --tail=200 alloy
docker compose exec alloy ls -l /var/run/docker.sock
docker inspect loki-stack-alloy-1 --format '{{json .Mounts}}'
重点检查 Docker socket 是否挂载、Alloy 配置是否加载,以及宿主机是否使用了不同的 Docker socket 路径。
docker system df
docker compose exec loki du -sh /loki/*
docker compose logs --tail=300 loki | grep -iE 'compactor|retention|delete|error'
确认 retention_enabled、24 小时索引周期和 Compactor 工作目录都正确。保留策略不会立刻释放全部空间,删除有延迟,因此磁盘告警不能等到 95% 才触发。
先缩短时间范围,并至少加上 service_name、compose_project 或 host。再检查是否把高基数字段做成标签。单机 VPS 上不应把“所有容器 30 天全文正则搜索”当作常规查询。
curl -fsS http://127.0.0.1:3000/api/health
sudo journalctl -u caddy -n 100 --no-pager
sudo caddy validate --config /etc/caddy/Caddyfile
如果本机 3000 健康而公网仍失败,再检查 Caddy 域名、证书和反向代理地址。
- 域名已指向 VPS,80/443 可达;
- 3000、3100 未向公网开放;
- Loki、Grafana、Alloy 使用固定版本;
- Compose、Loki 和 Alloy 配置检查通过;
- Grafana 管理员密码已更换并禁止匿名访问;
- 测试日志可以通过 LogQL 查到;
- 标签中没有用户 ID、请求 ID等高基数值;
- 30 天保留和磁盘告警已启用;
- 错误率告警已发送到真实通知渠道;
- 离线备份包含 Loki、Grafana、Alloy 和配置;
- 已在临时环境完成一次恢复演练;
- 升级操作每次只改一个组件。
少量 Docker 日志可从 4 GB 内存 VPS 起步,但这不是硬性下限。查询时间跨度、并发、日志量和标签基数都会影响内存。2 GB 机器可以实验,不建议同时承担重要业务和长期日志查询。
不完全能。Loki 擅长按标签筛选并扫描日志内容,资源成本较低;Elasticsearch/OpenSearch 更适合复杂全文索引、聚合和安全分析。选择取决于查询方式,不只是机器配置。
保留由 Compactor 异步执行,删除标记和实际清理之间存在延迟。还要确认 TSDB 索引周期为 24 小时、Compactor 目录持久化且没有报错。
可以,但不能直接公开 3100。应使用带 TLS 和认证的网关或反向代理,限制来源,并计算跨主机日志流量。需要多租户隔离时,还要设计租户头和权限边界。
新部署优先使用 Alloy。它能组合发现、重标记、日志、指标和链路采集组件,更适合作为长期统一采集代理。迁移前应按官方文档逐项对照现有 Promtail 阶段。
本文的文件系统离线归档需要短暂停写,以获得一致快照。若不能中断,应采用兼容对象存储、版本控制或存储快照,并针对该方案单独做一致性与恢复测试。
