Puter Docker Compose 部署指南
Puter 26.10.1 Docker Compose 六服务编排从零部署指南:MariaDB+Valkey+DynamoDB+RustFS+Caddy 全栈配置生成、sharp 修复镜像构建、dynamo 权限崩溃排障实录、账号注册登录与数据持久化,Ubuntu 24.04 单机实测。
Puter Docker Compose 部署指南
目标:在一台 Linux 服务器上用 Docker Compose 从零部署一套完整可用的 Puter 26.10.1 自托管个人云(WebOS)——含 MariaDB、Valkey、DynamoDB Local、RustFS 对象存储与 Caddy 反代的六服务编排,覆盖配置生成、启动验证、账号注册、数据持久化与运维命令。已在 Ubuntu 24.04(x86_64、Docker 29.9.2)单机环境实测通过。
一、环境信息
本文所有命令与回显均在以下真实环境实测(2026-11-01):
- 操作系统:Ubuntu 24.04.5 LTS(x86_64)
- Docker:29.9.2(含 Compose v5.6.1 插件)
- Puter 版本:26.10.1(官方最新稳定版,Releases 源 2026-10-25 发布;镜像
ghcr.io/heyputer/puter:main同源构建) - 依赖服务:mariadb:11、valkey/valkey:8-alpine、amazon/dynamodb-local:latest、rustfs/rustfs:latest、caddy:2.11-alpine、amazon/aws-cli:latest
- 服务器路径规划:部署目录
/opt/puter/(compose 文件、配置、数据均在其下) - 对外端口:
8140(映射到 Caddy 的 80;生产可改回 80/443) - 访问地址:
http://puter.localhost:8140(单机体验用*.localhost保留域名,无需真实 DNS)
💡 说明:Puter 是多服务编排型应用,单个
puter主服务之外还需要关系库、缓存、KV、对象存储四类依赖。官方 compose 把这些全部编排为一个项目,官方文档称之为「单主机上最接近生产的部署形态」。产品定位与同类对比见 《Puter 介绍与选型指南》。
二、镜像选型说明
Puter 官方发布渠道与各服务镜像的选型依据(2026-11-01 实查):
| 角色 | 镜像 | 选型说明 |
|---|---|---|
| 主应用 | ghcr.io/heyputer/puter | 官方 GHCR 镜像,仓库 CI 构建;官方 compose 默认拉 main 标签 |
| 关系数据库 | mariadb:11 | 官方 compose 固定版本;生产自托管受支持默认(PostgreSQL 为社区贡献路径) |
| 缓存 | valkey/valkey:8-alpine | Redis 兼容;官方 compose 以 cluster 模式单节点运行 |
| KV 数据库 | amazon/dynamodb-local:latest | AWS 官方本地版 DynamoDB,承载 puter.kv 能力 |
| 对象存储 | rustfs/rustfs:latest | S3 兼容;官方文档注明可用 MinIO 平替 |
| 桶初始化 | amazon/aws-cli:latest | 一次性任务:建桶 + 写 CORS,完成后退出 |
| 反向代理 | caddy:2.11-alpine | 80/443 入口,按 Host 透传给主应用 |
⚠️ 重要实测发现(26.10.1 时间窗):直接运行官方镜像启动时会立即崩溃,日志报
Cannot find module 'sharp'——官方镜像缺少 sharp 原生图像处理依赖。解决办法有两个:其一,基于官方镜像补装依赖后自建镜像(本文采用,Dockerfile 仅一行RUN cd /opt/puter && npm install sharp --no-audit --no-fund);其二,关注官方镜像后续修复版本,若新版已内置 sharp 则直接用官方镜像。自建镜像的完整操作见「四、部署步骤」4.3 节,崩溃日志全文见「十一、常见问题」Q1。
镜像体积实测参考:补装 sharp 后的主应用镜像 2.36GB,mariadb:11 458MB,rustfs 419MB,aws-cli 651MB,valkey 61.6MB——全栈磁盘占用约 4GB,建议预留 20GB(含数据增长)。
三、前置条件
- Docker 与 compose 插件(官方文档明确这是唯一需要自装的东西),验证:
docker compose version
# 实测输出:
# Docker Compose version v5.6.1
- 端口规划:本文用宿主
8140对外(避免与服务器上既有 80 冲突);生产部署建议 80/443 直出。确认端口未被占用:
ss -ltn | grep 8140 || echo 'port 8140 free'
# 实测输出(空闲时):
# port 8140 free
-
目录与权限:创建部署目录
/opt/puter/;Puter 主服务进程以 UID/GID 1000 运行,DynamoDB 容器显式以user: "1000:1000"运行——数据子目录必须归 UID 1000 所有,否则会在首次启动时踩 SQLite 权限坑(详见「十一、常见问题」Q2,这是本机实测踩过的真实故障)。 -
域名说明:单机体验无需域名(浏览器对
*.localhost自动解析到本机);公网生产部署需要一条通配符 DNS 记录(*.your-domain.com→ 服务器 IP),因为 Puter 按api./site./app.等子域名路由流量。
四、部署步骤
4.1 获取编排文件
推荐从官方仓库直接取 docker-compose.yml 与 caddy/Caddyfile(与官方一键安装脚本下载的是同一份):
mkdir -p /opt/puter/caddy && cd /opt/puter
wget -O docker-compose.yml https://raw.githubusercontent.com/HeyPuter/puter/main/install/docker-compose.yml
wget -O caddy/Caddyfile https://raw.githubusercontent.com/HeyPuter/puter/main/install/caddy/Caddyfile
# 实测输出(wget 两行进度条省略,退出码均为 0)
若服务器访问 GitHub 缓慢,可先在本地下载后 scp 上传,或使用镜像加速渠道下载后校验内容。本文实测用的编排文件为官方 install/ 目录版本改写:显式固定项目名 puter-26.10.1-stack 与自定义网络子网(多套栈并存时防止项目名冲突互删),并把宿主端口改为 8140。核心服务定义如下(完整文件可对照官方原版):
name: puter-26.10.1-stack
services:
valkey:
image: valkey/valkey:8-alpine
container_name: puter-26.10.1-valkey
restart: unless-stopped
command:
- sh
- -c
- |
valkey-server \
--port 6379 \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--cluster-require-full-coverage no \
--cluster-announce-ip valkey \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379 \
--appendonly yes \
--save "60 1" &
SERVER_PID=$$!
until valkey-cli -p 6379 PING > /dev/null 2>&1; do sleep 0.5; done
if ! valkey-cli -p 6379 CLUSTER NODES | grep -q '0-16383'; then
valkey-cli -p 6379 CLUSTER ADDSLOTSRANGE 0 16383
fi
wait $$SERVER_PID
volumes:
- ./puter/data/valkey:/data:z
healthcheck:
test: ["CMD-SHELL", "valkey-cli -p 6379 cluster info | grep -q cluster_state:ok"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
mariadb:
image: mariadb:11
container_name: puter-26.10.1-mariadb
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: ${MARIADB_DATABASE:-puter}
MARIADB_USER: ${MARIADB_USER:-puter}
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
volumes:
- ./puter/data/mariadb:/var/lib/mysql:z
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
dynamo:
image: amazon/dynamodb-local:latest
container_name: puter-26.10.1-dynamo
restart: unless-stopped
user: "1000:1000"
working_dir: /home/dynamodblocal
command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/home/dynamodblocal/data"]
volumes:
- ./puter/data/dynamo:/home/dynamodblocal/data:z
s3:
image: rustfs/rustfs:latest
container_name: puter-26.10.1-s3
restart: unless-stopped
environment:
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-puter}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY}
volumes:
- ./puter/data/s3:/data:z
healthcheck:
test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://localhost:9000/health || exit 1"]
interval: 5s
timeout: 3s
retries: 20
start_period: 5s
s3-init:
image: amazon/aws-cli:latest
container_name: puter-26.10.1-s3-init
depends_on:
s3:
condition: service_healthy
environment:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:-puter}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
AWS_DEFAULT_REGION: us-east-1
entrypoint:
- /bin/sh
- -c
- |
set -e
endpoint=http://s3:9000
bucket=${S3_BUCKET:-puter-local}
if aws --endpoint-url "$$endpoint" s3api head-bucket --bucket "$$bucket" 2>/dev/null; then
echo "bucket $$bucket already exists"
else
echo "creating bucket $$bucket"
aws --endpoint-url "$$endpoint" s3 mb "s3://$$bucket"
fi
cat > /tmp/cors.json <<'EOF'
{
"CORSRules": [
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "HEAD", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "x-amz-request-id"],
"MaxAgeSeconds": 3600
}
]
}
EOF
aws --endpoint-url "$$endpoint" s3api put-bucket-cors \
--bucket "$$bucket" --cors-configuration file:///tmp/cors.json
echo "bucket CORS configured"
restart: "no"
puter:
image: puter-26.10.1-fixed:local
pull_policy: never
container_name: puter-26.10.1-app
restart: unless-stopped
depends_on:
valkey:
condition: service_healthy
mariadb:
condition: service_healthy
dynamo:
condition: service_started
s3-init:
condition: service_completed_successfully
expose:
- "4100"
environment:
PUID: 1000
PGID: 1000
volumes:
- ./puter/config:/etc/puter:z
- ./puter/data/puter:/var/puter:z
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:4100/ || exit 1
interval: 30s
timeout: 3s
retries: 3
start_period: 30s
caddy:
image: caddy:2.11-alpine
container_name: puter-26.10.1-caddy
restart: unless-stopped
depends_on:
puter:
condition: service_started
ports:
- "${HTTP_PORT:-80}:80"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro,z
- ./puter/tls:/etc/caddy/tls:ro,z
- ./puter/data/caddy:/data:z
healthcheck:
test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://127.0.0.1:2019/config/ >/dev/null || exit 1"]
interval: 10s
timeout: 3s
retries: 5
start_period: 5s
networks:
default:
name: puter-26.10.1-net
ipam:
config:
- subnet: 10.89.65.0/24
编排要点逐项解读:
- 启动顺序用
depends_on+ 条件严格控制:主应用等 valkey/mariadb 健康后才启动,等 s3-init 一次性任务成功(建桶 + CORS)完成后才启动——这避免了「主应用先起、依赖未就绪」的启动竞态。 - Valkey 以单节点 cluster 模式运行:Puter 的 Redis 客户端按 cluster 协议连接,entrypoint 脚本负责起服务后自动把全部 16384 个槽位分配给自己(
CLUSTER ADDSLOTSRANGE 0 16383),健康检查验证cluster_state:ok。 - DynamoDB 显式
user: "1000:1000":以非 root 运行更安全,代价是数据目录属主必须是 UID 1000(见 4.4 步骤与常见问题 Q2)。 - Caddy 反代
puter:4100:主应用只expose不映射端口,所有对外流量走 Caddy;Caddyfile 按 Host 透传(s3.子域转发给对象存储做预签名直传,其余全部给主应用),flush_interval -1保证 SSE 与 socket.io 流式响应不被缓冲。 - 上传体积上限:Caddyfile 的
request_body max_size 1024MiB限制单请求 1 GiB,Puter 客户端对大文件分块上传,无需调大。
4.2 生成密钥与配置文件(.env + config.json)
Puter 读取两份配置:.env(compose 注入依赖服务)与 puter/config/config.json(主应用读取)。两份文件里的 MariaDB 密码与 S3 密钥必须一致,否则数据库首次初始化用一个密码、主应用拿另一个密码登录,报 ER_ACCESS_DENIED_ERROR(官方文档原文警示)。整段一次性执行(在 /opt/puter/ 下):
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 32)
MARIADB_PASSWORD=$(openssl rand -hex 32)
S3_SECRET_KEY=$(openssl rand -hex 32)
JWT_SECRET_V2=$(openssl rand -hex 64)
URL_SIGNATURE_SECRET=$(openssl rand -hex 64)
cat > .env <<EOF
HTTP_PORT=8140
MARIADB_ROOT_PASSWORD=$MARIADB_ROOT_PASSWORD
MARIADB_DATABASE=puter
MARIADB_USER=puter
MARIADB_PASSWORD=$MARIADB_PASSWORD
S3_ACCESS_KEY=puter
S3_SECRET_KEY=$S3_SECRET_KEY
S3_BUCKET=puter-local
EOF
mkdir -p caddy puter/config puter/data puter/tls
cat > puter/config/config.json <<EOF
{
"domain": "puter.localhost",
"protocol": "http",
"pub_port": 8140,
"env": "prod",
"static_hosting_domain": "site.puter.localhost",
"static_hosting_domain_alt": "host.puter.localhost",
"private_app_hosting_domain": "app.puter.localhost",
"private_app_hosting_domain_alt": "dev.puter.localhost",
"jwt_secret_v2": "$JWT_SECRET_V2",
"url_signature_secret": "$URL_SIGNATURE_SECRET",
"database": {
"engine": "mysql",
"host": "mariadb",
"port": 3306,
"user": "puter",
"password": "$MARIADB_PASSWORD",
"database": "puter",
"migrationPaths": ["/opt/puter/dist/src/backend/clients/database/migrations/mysql"]
},
"redis": {
"startupNodes": [{ "host": "valkey", "port": 6379 }],
"tls": false
},
"dynamo": {
"endpoint": "http://dynamo:8000",
"bootstrapTables": true,
"aws": {
"access_key": "fake",
"secret_key": "fake",
"region": "us-east-1"
}
},
"s3": {
"s3Config": {
"endpoint": "http://s3:9000",
"publicEndpoint": "http://s3.puter.localhost",
"accessKeyId": "puter",
"secretAccessKey": "$S3_SECRET_KEY",
"region": "us-east-1",
"forcePathStyle": true
}
},
"s3_bucket": "puter-local",
"s3_region": "us-east-1",
"providers": {
"ollama": { "enabled": false }
},
"meteringEnforcement": {
"subscriptions": false
},
"trust_proxy": 1
}
EOF
chmod 600 .env
配置要点逐项说明:
domain+pub_port:主应用对外呈现的域名与端口。单机体验写puter.localhost+8140;公网部署改为你的域名+80(走 Caddy 时主应用看到的仍是透传 Host)。jwt_secret_v2/url_signature_secret:会话令牌与预签名 URL 的签名密钥,务必随机生成且不外泄;轮换会使全部已登录会话失效。database.migrationPaths:指向镜像内 MySQL 迁移脚本目录,首次启动自动建全部表。dynamo.bootstrapTables: true:首次启动自动建 KV 所需表,无需手工初始化。s3.forcePathStyle: true:自托管 S3 兼容存储(RustFS/MinIO)必须用路径风格寻址而非子域名寻址。trust_proxy: 1:信任前方 1 层反代(Caddy)传来的X-Forwarded-*头,获取真实客户端 IP。unlimitedMetering未开启时的行为:自托管默认计量会在账户月度免费额度用尽后拒绝写入类操作(402insufficient_funds),自托管场景建议追加"unlimitedMetering": true(参数详解见 《Puter 26.10.1 配置参数详解》)。
4.3 构建修复版主应用镜像(补 sharp 依赖)
在 /opt/puter/ 下建一个单行 Dockerfile 并构建:
cat > Dockerfile.patch <<'EOF'
FROM ghcr.io/heyputer/puter:main
# 修复 26.10.1 官方镜像缺失 sharp 原生依赖导致启动崩溃的问题
RUN cd /opt/puter && npm install sharp --no-audit --no-fund
EOF
docker build -f Dockerfile.patch -t puter-26.10.1-fixed:local .
构建过程实测输出(节选首尾):
[+] Building 96.7s (7/7) FINISHED
=> [internal] load build definition from Dockerfile.patch
=> [1/3] FROM ghcr.io/heyputer/puter:main
=> [2/3] RUN cd /opt/puter && npm install sharp --no-audit --no-fund
=> [3/3] exporting to image
=> => naming to docker.io/library/puter-26.10.1-fixed:local
⚠️ 网络说明:构建时
npm install sharp需要访问 npm registry,国内网络环境下可能缓慢或超时。实测降级链:官方 npm registry 直连失败时,可给构建加代理(在 Dockerfile 中声明ARG HTTP_PROXY后以docker build --build-arg HTTP_PROXY=http://192.168.1.72:7897 -f Dockerfile.patch -t puter-26.10.1-fixed:local .构建),或先在有网环境构建好docker save导出再入网导入。本文实测走代理通道完成。
4.4 预置数据目录权限并启动
先预置数据子目录,并把 DynamoDB 数据目录属主改为 UID 1000(跳过这一步会触发首次启动崩溃循环,实录见常见问题 Q2):
mkdir -p puter/data/{mariadb,dynamo,s3,valkey,caddy,puter}
chown -R 1000:1000 puter/data/dynamo
拉起全栈:
docker compose up -d
# 实测输出:
# [+] Running 8/8
# ✔ Network puter-26.10.1-net Created
# ✔ Container puter-26.10.1-valkey Healthy
# ✔ Container puter-26.10.1-mariadb Healthy
# ✔ Container puter-26.10.1-s3 Healthy
# ✔ Container puter-26.10.1-s3-init Exited
# ✔ Container puter-26.10.1-dynamo Started
# ✔ Container puter-26.10.1-app Started
# ✔ Container puter-26.10.1-caddy Started
💡 技术拓展:
s3-init显示Exited不是错误——它是设计为一次性运行的任务容器(建桶 + 写 CORS 配置后退出,restart: "no"),退出码 0 即成功。这种「init 容器」模式在依赖预配置场景很常用:数据库 schema 迁移、证书签发、桶初始化等都适合做成跑完即退的一次性容器。
4.5 首次启动观察与就绪判定
首次启动时主应用要跑完全部 49 个数据库迁移脚本,耗时 1~2 分钟。实时观察日志:
docker compose logs -f puter
实测首启日志(输出节选(实测首尾)):
[config] defaults from /opt/puter/config.default.json
[config] override from /etc/puter/config.json
connecting to redis from config
[redis] cluster transport connected
[redis] cluster ready
[mysql] connected to primary
[mysql] running migrations from /opt/puter/dist/src/backend/clients/database/migrations/mysql: 49 file(s)
[mysql] applied mysql_mig_1.sql (501 statements)
[mysql] applied mysql_mig_2.sql (10 statements)
...
[mysql] applied mysql_mig_48.sql (1 statements)
[mysql] applied mysql_mig_49.sql (6 statements)
[email] no email transport configured — send() will fail until configured
[s3] configured with remote endpoint
[prelude] no apiKey configured — SMS phone verification is disabled
************************************************************
* Puter is now live at: http://puter.localhost:8140
************************************************************
PuterServer has fully booted.
判定就绪的三条验证(全部实测):
# 1. 全部服务健康(s3-init 为正常退出态)
docker compose ps --format 'table {{.Name}}\t{{.Image}}\t{{.Status}}'
# 实测输出:
# NAME IMAGE STATUS
# puter-26.10.1-app puter-26.10.1-fixed:local Up 12 minutes (healthy)
# puter-26.10.1-caddy caddy:2.11-alpine Up 35 minutes (healthy)
# puter-26.10.1-dynamo amazon/dynamodb-local:latest Up 12 minutes
# puter-26.10.1-mariadb mariadb:11 Up 35 minutes (healthy)
# puter-26.10.1-s3 rustfs/rustfs:latest Up 35 minutes (healthy)
# puter-26.10.1-valkey valkey/valkey:8-alpine Up 35 minutes (healthy)
# 2. Web 入口返回 200
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8140/
# 实测输出:
# 200
# 3. 数据库 46 张业务表就绪
docker exec puter-26.10.1-mariadb sh -c "mariadb -uputer -p\$MARIADB_PASSWORD puter -e 'SHOW TABLES;'" | wc -l
# 实测输出:
# 46
4.6 注册账号并登录 Web 桌面
浏览器打开 http://puter.localhost:8140,首次进入会弹出注册界面:输入用户名(如 admin)、密码,即可创建账号并进入 Web 桌面。也可以用 REST API 注册与登录(本文实测):
# 注册 admin 账号(演示口令 q1w2e3r4)
curl -s -X POST http://localhost:8140/signup \
-H 'Content-Type: application/json' \
-d '{"username":"admin","email":"admin@puter.localhost","password":"q1w2e3r4"}'
# 实测输出(首次注册成功时返回用户对象;用户名已存在时返回):
# {"error":"This username is not available.","message":"This username is not available.","code":"username_already_in_use"}
# 登录拿会话令牌
curl -s -X POST http://localhost:8140/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}' -o /tmp/login.json -w '%{http_code}\n'
# 实测输出:
# 200
python3 -c "import json;d=json.load(open('/tmp/login.json'));print(sorted(d.keys()),d['next_step'])"
# 实测输出:
# ['next_step', 'proceed', 'token', 'user'] complete
登录成功返回 {proceed: true, next_step: "complete", token: <JWT>, user: {...}},浏览器侧则直接种入会话 Cookie 进入桌面。API 的进阶调用(文件读写、KV、AI)见 《Puter 26.10.1 API 使用详解》。
五、数据持久化说明
全部状态落在 /opt/puter/puter/data/ 下的子目录(bind mount 进各服务),备份即备份该目录:
| 服务 | 宿主目录 | 容器路径 | 内容 |
|---|---|---|---|
| mariadb | puter/data/mariadb | /var/lib/mysql | 用户/文件元数据/应用等 46 张表(ibd 文件) |
| dynamo | puter/data/dynamo | /home/dynamodblocal/data | KV 数据(SQLite 单文件 shared-local-instance.db) |
| s3 | puter/data/s3 | /data | 文件实体对象(rustfs 桶 puter-local) |
| valkey | puter/data/valkey | /data | 缓存/AOF 持久化(可丢失重建,不备份亦可) |
| caddy | puter/data/caddy | /data | 反代运行状态 |
| puter | puter/data/puter | /var/puter | 主应用本地状态 |
实测目录清单:
puter/data/:
caddy
dynamo
mariadb
puter
s3
valkey
⚠️ 注意:
puter/config/config.json与.env虽不在 data 目录,但属于「丢了就无法恢复登录」的关键配置(JWT 密钥丢失 = 全部会话失效),备份时一并纳入。文件实体的真实去向是 S3 桶:数据库里只存元数据条目(fsentry),因此完整备份 =mariadb + dynamo + s3三个子目录。
六、环境变量表
.env 全部变量(compose 注入依赖服务):
| 变量 | 实测值/示例 | 说明 |
|---|---|---|
HTTP_PORT | 8140 | Caddy 对宿主映射端口(默认 80) |
MARIADB_ROOT_PASSWORD | 随机 64 位 hex | MariaDB root 密码(仅初始化时使用) |
MARIADB_DATABASE | puter | 业务库名 |
MARIADB_USER | puter | 业务账号 |
MARIADB_PASSWORD | 随机 64 位 hex | 业务账号密码,须与 config.json 中一致 |
S3_ACCESS_KEY | puter | 对象存储访问键(同时注入 rustfs 与 aws-cli) |
S3_SECRET_KEY | 随机 64 位 hex | 对象存储密钥,须与 config.json 中一致 |
S3_BUCKET | puter-local | 文件实体存储桶名 |
主应用容器另有 PUID/PGID(1000/1000)控制运行身份。系统级第三方密钥(如接入 OpenAI/Claude 的 providers.*.apiKey)属敏感值,通过 config.json 配置时建议以环境变量注入方式管理而非明文落盘。
七、配置参数说明
主应用配置集中在 puter/config/config.json(首次启动后按需修改,改后 docker compose restart puter 生效)。本文 4.2 节给出的即最小可用生产配置,参数分五组:
- 域与协议组:
domain、protocol、pub_port、static_hosting_domain、private_app_hosting_domain等子域路由参数——决定 Web 桌面与托管站点以什么域名对外; - 凭据组:
jwt_secret_v2、url_signature_secret——会话与签名安全; - 存储组:
database(MySQL/MariaDB 连接与迁移路径)、dynamo(KV 端点与建表开关)、s3(对象存储端点/密钥/桶)、redis(缓存集群节点); - 账号策略组:
min_pass_length、disable_user_signup、captcha、oidc(第三方登录)、blockedEmailDomains、storage_capacity/is_storage_limited(配额); - 计量组:
unlimitedMetering、meteringEnforcement——自托管场景必查(不关会在额度用尽后 402 拒写)。
全部 70 个参数键的分类详解、默认值与实测验证见 《Puter 26.10.1 配置参数详解》。
八、配置文件挂载
| 宿主路径 | 容器路径 | 读写 | 用途 |
|---|---|---|---|
caddy/Caddyfile | /etc/caddy/Caddyfile | 只读 | 反代路由规则(TLS 块默认注释,启用方式见文件头注释) |
puter/config/config.json | /etc/puter/config.json | 读写 | 主应用配置(优先级高于镜像内置 config.default.json) |
puter/data/* | 各服务数据目录 | 读写 | 持久化(见第五节) |
puter/tls/ | /etc/caddy/tls | 只读 | 通配符证书(fullchain.pem + privkey.pem,启用 TLS 时放入) |
修改配置后的生效方式:
docker compose restart puter
# 主应用重启约 30~60 秒(健康检查 start_period 30s),期间 Web 不可访问
九、等价 docker run
不用 compose 时,两条核心容器的等价启动命令如下(依赖服务同理逐个启动,此处示范主应用与反代;实际生产仍建议 compose 统一编排):
# 主应用(前置:mariadb/valkey/dynamo/s3 已按各自官方镜像启动且网络互通)
docker run -d --name puter-26.10.1-app \
--network puter-26.10.1-net \
-e PUID=1000 -e PGID=1000 \
-v /opt/puter/puter/config:/etc/puter \
-v /opt/puter/puter/data/puter:/var/puter \
puter-26.10.1-fixed:local
# Caddy 反代(对外 8140 → 容器 80)
docker run -d --name puter-26.10.1-caddy \
--network puter-26.10.1-net \
-p 8140:80 \
-v /opt/puter/caddy/Caddyfile:/etc/caddy/Caddyfile:ro \
-v /opt/puter/puter/data/caddy:/data \
caddy:2.11-alpine
注意等价手拉方式没有 depends_on 编排能力,需自行保证启动顺序与就绪条件,这也是推荐 compose 的原因。
十、常用运维命令
cd /opt/puter
# 查看整体状态
docker compose ps
# 实测输出(六服务一 exited,详见 4.5 节)
# 跟踪/查看主应用日志
docker compose logs -f puter
docker compose logs --since 10m puter
# 重启单个服务(改 config.json 后)
docker compose restart puter
# 整栈停止(保数据)与再启动
docker compose stop
docker compose up -d
# 整栈更新(拉官方新镜像后重建主应用)
docker compose pull puter
docker compose up -d puter
升级注意事项:先备份 puter/data/ 与配置再升级;版本跨度大时迁移脚本会在首启自动增量执行(日志可见 applied mysql_mig_NN.sql 行数递增);回滚 = 用旧镜像 tag 重建容器 + 恢复备份的数据目录(数据库降级官方不支持,回滚前必须有备份)。
十一、常见问题
Q1:启动立即崩溃,日志报 Cannot find module 'sharp'(本机实测实录)
- 现象:
docker compose up -d后主应用反复重启,日志出现:
node:internal/modules/cjs/loader:1145
throw err;
^
Error: Cannot find module 'sharp'
Require stack:
- /opt/puter/dist/src/backend/src/services/drivers/thumb.js
at Module._resolveFilename (node:internal/modules/cjs/loader:1142:15)
- 定位:进镜像检查依赖目录确认 sharp 缺失——官方镜像 CI 构建未包含该原生依赖(26.10.1 时间窗实测)。
- 解决:按 4.3 节用单行 Dockerfile
RUN cd /opt/puter && npm install sharp构建修复镜像并替换 compose 中的image:字段,重启后正常。
Q2:主应用启动后进入崩溃循环,日志报 DynamoDB 连接超时(本机实测实录)
- 现象:所有服务 healthy、Web 也能短暂打开,但主应用每隔一两分钟重启一次,日志反复出现:
uncaughtException (unhandledRejection) Error [TimeoutError]: Connection timed out after 5000 ms
at ClientRequest.<anonymous> (/opt/puter/node_modules/@smithy/node-http-handler/dist-cjs/index.js:97:26)
...
'$metadata': { attempts: 3, totalRetryDelay: 74 }
- 定位:
@smithy(AWS SDK)连的是 DynamoDB(dynamo:8000)。查 DynamoDB 容器日志发现根因不是网络不通,而是存储层初始化失败:
WARNING: [sqlite] cannot open DB[436]: com.almworks.sqlite4java.SQLiteException: [14] unable to open database file
SEVERE: [sqlite] SQLiteQueue[shared-local-instance.db]: error running job queue
DynamoDB Local 底层用 SQLite 落盘,容器以 user: "1000:1000" 运行,而宿主数据目录 puter/data/dynamo 默认属主是 root——SQLite 打不开数据库文件后 API 请求挂起,主应用侧表现为 5 秒超时且未捕获异常退出(退出码 0),restart 策略反复拉起。
- 解决:
chown -R 1000:1000 puter/data/dynamo后重启 dynamo 与主应用,SQLite 报错消失,主应用 RestartCount 不再增长,栈稳定运行(实测修复后dynamo HTTP 400——无 body 的 GET 返回 400 属正常响应,说明服务恢复应答)。
Q3:改完 admin 密码后登录仍提示 Incorrect password(本机实测实录)
- 现象:直接更新数据库
user.password字段(bcrypt 哈希)后,POST /login仍返回 401password_mismatch;但用相同哈希在容器内跑bcrypt.compare结果为 true。 - 定位:主应用进程内有用户对象缓存,直接改库后进程仍持有旧哈希。
- 解决:
docker compose restart puter清进程缓存后登录立即恢复 200。运维启示:凡绕过应用直接改库的操作,改完必须重启主应用。
Q4:账户操作返回 402 insufficient_funds
自托管默认开启用量计量,账户每月免费额度(官方标称 0.5 美元量级,约 2.5 GiB 下行)用尽后写入类操作被拒。自托管环境在 config.json 加 "unlimitedMetering": true 并重启主应用即可(用量仍记账展示,但不再拒绝请求)。
Q5:主应用日志提示 ER_ACCESS_DENIED_ERROR
.env 与 puter/config/config.json 中 MariaDB 密码不一致(官方文档原文警示的典型事故):MariaDB 用 A 密码初始化了用户,主应用拿 B 密码连接。首次初始化前核对两份文件;若已初始化,最简单做法是同步密码后重置 mariadb 数据目录重新初始化(会清数据,务必先确认是全新部署)。
Q6:公网部署后上传/下载跨域失败或签名错误
检查三点:Caddyfile 中 s3. 子域匹配规则未被改坏;config.json 的 s3.publicEndpoint 与实际访问域一致;桶的 CORS 配置存在(s3-init 任务成功执行过,可用 aws-cli get-bucket-cors 验证)。预签名 URL 的签名基于访问域计算,域不一致必然 403。