Puter Docker Compose 部署指南

Puter 26.10.1 Docker Compose 六服务编排从零部署指南:MariaDB+Valkey+DynamoDB+RustFS+Caddy 全栈配置生成、sharp 修复镜像构建、dynamo 权限崩溃排障实录、账号注册登录与数据持久化,Ubuntu 24.04 单机实测。

PuterPuterDockerCompose文件传输文件服务器2026/11/01

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-alpineRedis 兼容;官方 compose 以 cluster 模式单节点运行
KV 数据库amazon/dynamodb-local:latestAWS 官方本地版 DynamoDB,承载 puter.kv 能力
对象存储rustfs/rustfs:latestS3 兼容;官方文档注明可用 MinIO 平替
桶初始化amazon/aws-cli:latest一次性任务:建桶 + 写 CORS,完成后退出
反向代理caddy:2.11-alpine80/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(含数据增长)。

三、前置条件

  1. Docker 与 compose 插件(官方文档明确这是唯一需要自装的东西),验证:
docker compose version
# 实测输出:
# Docker Compose version v5.6.1
  1. 端口规划:本文用宿主 8140 对外(避免与服务器上既有 80 冲突);生产部署建议 80/443 直出。确认端口未被占用:
ss -ltn | grep 8140 || echo 'port 8140 free'
# 实测输出(空闲时):
# port 8140 free
  1. 目录与权限:创建部署目录 /opt/puter/;Puter 主服务进程以 UID/GID 1000 运行,DynamoDB 容器显式以 user: "1000:1000" 运行——数据子目录必须归 UID 1000 所有,否则会在首次启动时踩 SQLite 权限坑(详见「十一、常见问题」Q2,这是本机实测踩过的真实故障)。

  2. 域名说明:单机体验无需域名(浏览器对 *.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 未开启时的行为:自托管默认计量会在账户月度免费额度用尽后拒绝写入类操作(402 insufficient_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 进各服务),备份即备份该目录:

服务宿主目录容器路径内容
mariadbputer/data/mariadb/var/lib/mysql用户/文件元数据/应用等 46 张表(ibd 文件)
dynamoputer/data/dynamo/home/dynamodblocal/dataKV 数据(SQLite 单文件 shared-local-instance.db)
s3puter/data/s3/data文件实体对象(rustfs 桶 puter-local)
valkeyputer/data/valkey/data缓存/AOF 持久化(可丢失重建,不备份亦可)
caddyputer/data/caddy/data反代运行状态
puterputer/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_PORT8140Caddy 对宿主映射端口(默认 80)
MARIADB_ROOT_PASSWORD随机 64 位 hexMariaDB root 密码(仅初始化时使用)
MARIADB_DATABASEputer业务库名
MARIADB_USERputer业务账号
MARIADB_PASSWORD随机 64 位 hex业务账号密码,须与 config.json 中一致
S3_ACCESS_KEYputer对象存储访问键(同时注入 rustfs 与 aws-cli)
S3_SECRET_KEY随机 64 位 hex对象存储密钥,须与 config.json 中一致
S3_BUCKETputer-local文件实体存储桶名

主应用容器另有 PUID/PGID(1000/1000)控制运行身份。系统级第三方密钥(如接入 OpenAI/Claude 的 providers.*.apiKey)属敏感值,通过 config.json 配置时建议以环境变量注入方式管理而非明文落盘。

七、配置参数说明

主应用配置集中在 puter/config/config.json(首次启动后按需修改,改后 docker compose restart puter 生效)。本文 4.2 节给出的即最小可用生产配置,参数分五组:

  1. 域与协议组:domain、protocol、pub_port、static_hosting_domain、private_app_hosting_domain 等子域路由参数——决定 Web 桌面与托管站点以什么域名对外;
  2. 凭据组:jwt_secret_v2、url_signature_secret——会话与签名安全;
  3. 存储组:database(MySQL/MariaDB 连接与迁移路径)、dynamo(KV 端点与建表开关)、s3(对象存储端点/密钥/桶)、redis(缓存集群节点);
  4. 账号策略组:min_pass_length、disable_user_signup、captcha、oidc(第三方登录)、blockedEmailDomains、storage_capacity/is_storage_limited(配额);
  5. 计量组: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 仍返回 401 password_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。

十二、参考链接

  1. 官方自托管文档:https://github.com/HeyPuter/puter/blob/main/doc/self-hosting.md
  2. 官方一键安装脚本入口:https://puter.com/selfhost
  3. 官方镜像:https://github.com/HeyPuter/puter/pkgs/container/puter
  4. 版本发布页:https://github.com/HeyPuter/puter/releases
  5. 本专栏相关篇目:《Puter 介绍与选型指南》、《Puter 26.10.1 配置参数详解》、《Puter 26.10.1 API 使用详解》
Puter Docker Compose 部署指南 - Quzuo