SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

如何在Ubuntu 24.04 VPS上自托管MinIO

在一台 Ubuntu 24.04 VPS 上构建 MinIO S3 服务,涵盖验证源码、systemd、mc、预签名 URL 和 restic。注意:Community Edition 预编译文件路径已返回 410 Gone。

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

自托管 MinIO 对象存储能提供什么

MinIO 是支持 Amazon S3 API 的自托管对象存储。将 restic 或任何 S3 SDK 指向您自己的服务器,只需修改一个端点设置,客户端就无法感知差异。本指南将在 Ubuntu 24.04 上构建单节点部署:使用经过验证的源代码编译二进制文件,创建专用系统用户,配置一个不会将 root 凭据写入单元文件的 systemd 单元,并创建一个供 restic 备份数据的存储桶。

S3(简单存储服务)是一种 HTTP API,而不是文件系统。您可以使用 PUT 将对象按键写入存储桶,再使用 GET 获取对象;它不支持部分写入,也不支持重命名。备份工具适合这种模型,因为对象要么完整写入,要么根本没有写入。

单个节点只保存一份数据副本。这就是您需要承担的取舍。您只需支付 VPS 的费用,就能获得一个由自己控制的 S3 端点;同时也要承担云服务商原本负责的所有工作,包括更换故障磁盘和修补服务器软件。本文末尾附近的章节会明确说明这种取舍在什么情况下合理。

MinIO Community Edition 在 2026 年 10 月的状态

在基于此版本构建之前,请先阅读本节,因为情况最近发生了变化。2025 年 5 月,MinIO 从 Community Edition 的 Web 控制台中移除了管理功能。浏览器中保留的只是对象浏览器,因此现在必须使用 mc 命令行客户端管理存储桶和访问密钥。

2025 年晚些时候,MinIO 停止发布预编译的 Community Edition 二进制文件。项目 README 说明,Community Edition 现在仅以源代码形式分发;自 2026 年 4 月起,minio/minio 仓库已归档,并显示 THIS REPOSITORY IS NO LONGER MAINTAINED 通知。旧下载服务器也已停止提供 Community Edition 文件。截至 2026 年 10 月 10 日,本指南过去用于下载的每个 dl.min.io 路径,包括服务器二进制文件、客户端二进制文件及其校验和文件,均返回 410 Gone。这表示文件已被有意移除,而不是服务器中断。min.io 上的下载页面现在只提供 AIStor Server、AIStor Client 和 AIStor SDK。

源代码仍然存在,并且版本新于最后一个二进制文件。最后一个 Community Edition 版本是 RELEASE.2025-10-15T17-29-55Z,于 2025 年 10 月 16 日作为安全版本发布:该版本修复了服务账户和 STS 中通过会话策略绕过实现权限提升的问题,发布说明建议所有用户立即升级。最后一个预编译二进制文件是 2025 年 9 月的构建版本,因此没有包含此修复。本指南编译 10 月版本;由于仓库已经归档,这也是 Community Edition 将获得的最后一个修复。

这一事实决定了本指南后续内容。正因如此,这里的 MinIO 监听 127.0.0.1,并且只通过由您控制的代理访问互联网。

安装 MinIO 二进制文件并验证下载

MinIO 使用 Go 编写,供应商 README 通过一条 go install 命令构建它,该命令需要 Go 1.24 或更高版本。先从官方 tarball 安装 Go。2026 年 10 月 10 日,go.dev/dl 上的当前稳定版本是 go1.27.2,下面的哈希值是该页面为 linux-amd64 归档文件发布的值。如果选择更新的 Go 版本,请从同一页面获取其哈希值。-f 标志会让 curl 在发生 HTTP 错误时失败,而不是将错误页面保存为指定的文件名。否则,用户可能会解压 404 页面,然后疑惑它为什么不是归档文件。

cd /tmp
GOVER=go1.27.2
curl -fsSL "https://go.dev/dl/$GOVER.linux-amd64.tar.gz" -o go.tar.gz

比较两个哈希值,只比较哈希值。

published=ecbadb99091a3f46e31f5f934b068b1864eafa7995211b39eaddf76996045fe5
downloaded=$(sha256sum go.tar.gz | awk '{print $1}')
[ "$published" = "$downloaded" ] && echo "checksum ok"

明确这项检查能够证明什么。匹配表示下载完整,且内容未在传输过程中损坏或被篡改。但它不能证明供应商值得信任。这是另一个问题,没有任何 sha256sum 命令可以解决它。

sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version

现在以普通用户身份构建最终的社区版本。go install 会根据 Go 的公共校验和数据库 sum.golang.org,检查它下载的每个模块,包括 MinIO 自身的源代码。因此,遭到篡改或替换的模块会使构建因安全错误而停止,而不会生成二进制文件。

go install github.com/minio/minio@RELEASE.2025-10-15T17-29-55Z
sudo install -o root -g root -m 755 ~/go/bin/minio /usr/local/bin/minio
minio --version
go version -m /usr/local/bin/minio | head -n 3

minio --version 会在原本应显示版本名称的位置输出 DEVELOPMENT.GOGET,因为 go install 不会应用供应商用于为其二进制文件写入版本信息的构建标志。go version -m 会读取 Go 嵌入每个二进制文件中的模块信息,其 mod github.com/minio/minio 行会显示你构建的提交。此处出现 Permission denied 表示模式不正确,出现 command not found 表示 /usr/local/bin 不在你的 PATH 上。如果 VPS 的内存不足以完成编译,请在具有相同架构的另一台 Linux 计算机上运行相同的 go install,然后将二进制文件复制过来。

创建系统用户和数据目录

MinIO 接受来自网络的上传请求,因此不应以 root 身份运行。为它创建一个没有主目录且没有登录 shell 的账户。

sudo groupadd -r minio-user
sudo useradd -M -r -g minio-user -s /usr/sbin/nologin minio-user
sudo mkdir -p /var/lib/minio/data
sudo chown -R minio-user:minio-user /var/lib/minio
sudo chmod 750 /var/lib/minio

-r 创建 UID 小于 1000 的系统账户,使其不在普通用户使用的范围内。-M 不创建主目录,因为从不登录的账户无需保存文件。使用 id minio-user 检查结果,并使用 stat -c '%U %a' /var/lib/minio 检查;后者应输出 minio-user 750。

数据目录必须允许该用户写入,而不仅是读取。MinIO 首次启动时会在卷中创建 .minio.sys 目录,用于保存自身的配置。因此,如果目录归 root 所有,MinIO 会在启动期间退出,并显示以 permission denied 结尾的消息。以这种方式运行的每项服务都适用同一规则;VPS 上的最小权限服务用户对此有详细说明。

将 root 凭据放入环境文件

root 凭据可以访问每个存储桶,因此不应放在所有用户都可读的单元文件中。先以正确的模式创建文件,再写入内容。这样即使在极短时间内,密码也不会存在于可读文件中。

sudo install -o root -g root -m 600 /dev/null /etc/default/minio
printf 'MINIO_ROOT_USER=minio-root\nMINIO_ROOT_PASSWORD=%s\nMINIO_VOLUMES="/var/lib/minio/data"\nMINIO_OPTS="--address 127.0.0.1:9000 --console-address 127.0.0.1:9001"\n' "$(openssl rand -base64 24)" | sudo tee /etc/default/minio > /dev/null
sudo sed -n 's/^MINIO_ROOT_PASSWORD=//p' /etc/default/minio

tee 会截断现有文件,而不是重新创建文件,因此文件模式保持为 600,所有者保持为 root。这是有意为之的。systemd 会在降权为 User= 之前以 root 身份读取 EnvironmentFile,因此服务账户无需读取自己的凭据。服务运行后,使用 sudo -u minio-user cat /etc/default/minio 验证。该命令必须输出 Permission denied。

启动 MinIO 前,需要了解它的两个行为。在环境中既没有 MINIO_ROOT_USER,也没有 MINIO_ROOT_PASSWORD 时,MinIO 不会拒绝启动。它会使用文档中所述的默认凭据 minioadmin:minioadmin 启动。这是扫描器首先尝试的凭据对,MinIO 在使用它们时看起来完全正常。密码少于 8 个字符时则会被拒绝:MinIO 会在启动时退出,并报告凭据无效,因为访问密钥至少需要 3 个字符,密钥至少需要 8 个字符。

MINIO_VOLUMES 是数据路径,MINIO_OPTS 保存选项。绑定到 127.0.0.1 意味着此 VPS 外部的任何设备都无法访问 S3 API,这是正确的默认设置。稍后通过持有证书的代理按需开放访问。

编写 systemd 单元

创建 /etc/systemd/system/minio.service:

[Unit]
Description=MinIO object storage
Documentation=https://github.com/minio/minio
Wants=network-online.target
After=network-online.target

[Service]
User=minio-user
Group=minio-user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
Restart=always
RestartSec=5
LimitNOFILE=65536
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

EnvironmentFile 前没有前导 -,这是有意的,不是拼写错误。使用连字符时,systemd 会忽略缺失的文件,并继续启动 MinIO。因此,文件被删除或路径拼写错误时,系统会悄然启动一个使用 minioadmin:minioadmin 的服务器。不使用连字符时,缺失文件会在 MinIO 运行前使单元启动失败,journalctl -u minio 会显示 Failed to load environment files: No such file or directory。拒绝启动的单元比悄然接受默认密码的服务器更容易被发现。

$MINIO_VOLUMES 和 $MINIO_OPTS 有意不加引号,因为 systemd 会将未加引号的变量按空白字符拆分为独立参数。这样,MINIO_OPTS 中的四个单词会成为传递给 minio server 的四个参数。LimitNOFILE=65536 会提高文件描述符限制,因为每个打开的连接和每个打开的数据文件都会占用一个描述符,而默认值 1024 在高负载下会耗尽。

sudo systemctl daemon-reload
sudo systemctl enable --now minio
systemctl is-active minio
curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9000/minio/health/live

is-active 应输出 active,健康检查端点应返回 200。journalctl -u minio -n 20 --no-pager 会显示服务器正在监听的 API 地址。如果单元持续重启,systemd 会放弃并记录 Start request repeated too quickly。这表示 MinIO 每次尝试都会退出。原因会显示在该消息上方的几行中,因此请向上查看。

如需进一步隔离,请将 ProtectSystem=full 和 ProtectHome=true 添加到 [Service] 部分。这两项都需要主机内核提供挂载命名空间。在共享主机内核的容器虚拟化环境中,例如 OpenVZ 或 LXC,它们可能会失败,此时单元会报告 status=226/NAMESPACE。删除这两行后,单元即可启动。该单元本身是普通单元,VPS 上的 systemd 服务和计时器介绍了其余指令。

安装 mc 并验证完整往返

MinIO 客户端是 mc。不要使用 apt install mc 安装它。该软件包是 Midnight Commander,一款与 MinIO 无关的文件管理器。

dl.min.io 也不再提供该客户端,minio/mc 软件仓库自 2026 年 7 月起已归档,因此请使用相同的 Go 工具链构建其最后一个版本 RELEASE.2025-08-13T08-35-41Z。校验和数据库也覆盖此下载。

go install github.com/minio/mc@RELEASE.2025-08-13T08-35-41Z
sudo install -o root -g root -m 755 ~/go/bin/mc /usr/local/bin/mc

将服务器注册为别名,然后通过该别名传输一个对象。

MINIO_PASS=$(sudo sed -n 's/^MINIO_ROOT_PASSWORD=//p' /etc/default/minio)
mc alias set local http://127.0.0.1:9000 minio-root "$MINIO_PASS"
mc mb local/backups
echo "hello object storage" > /tmp/hello.txt
mc cp /tmp/hello.txt local/backups/hello.txt
mc ls local/backups
mc cat local/backups/hello.txt

mc ls 应列出 hello.txt 及其大小,mc cat 应输出 hello object storage。这次往返才是服务器正常工作的实际证明,因为它会发送与其他客户端相同的已签名 S3 请求。如果需要进一步确认,mc admin info local 会输出服务器状态。

现在再执行一次检查,此时服务器上仍然没有数据。

mc alias set defaultcheck http://127.0.0.1:9000 minioadmin minioadmin

此命令必须失败。如果命令成功,说明环境文件没有传递给进程,服务器仍在使用默认凭据。先修复此问题,再让其他任何操作访问这台机器。

mc 会以纯文本形式将别名存储在 ~/.mc/config.json 中,因此凭据会保存在执行该命令的用户的主目录内。在 sudo 下运行 mc 会将 root 凭据写入 /root/.mc/config.json。仅在一个管理员账户上保留 root 别名,并为每个应用分配独立的密钥。

使用预签名 URL 分发一个对象

预签名 URL 是一个附带签名和过期时间的普通 HTTPS 链接。任何持有该链接的人都可以获取这一个对象,无需账户,也无需客户端。

mc share download --expire 12h local/backups/hello.txt

输出会在查询字符串中包含 X-Amz-Signature 和 X-Amz-Expires。其中有两点容易让人意外。该链接根据您所使用别名中的端点生成。因此,在 127.0.0.1 上创建的别名会生成只能由此计算机打开的链接。对于您计划发送的链接,请在公共主机名上再创建一个别名。另一个问题是没有撤销按钮。签名会一直有效到过期,因此您唯一可用的控制手段是设置较短的有效期。S3 签名格式允许的最长有效期是 7 天。

为 restic 创建专用密钥和存储桶

root 凭据可以读取和删除所有存储桶,因此备份作业不能持有这些凭据。创建一个存储桶、一个仅限该存储桶的策略,以及一个不具备其他权限的用户。

mc mb local/restic
cat > /tmp/restic-rw.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
      "Resource": ["arn:aws:s3:::restic"]
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": ["arn:aws:s3:::restic/*"]
    }
  ]
}
EOF
RESTIC_KEY=$(openssl rand -base64 24)
mc admin policy create local restic-rw /tmp/restic-rw.json
mc admin user add local restic-backup "$RESTIC_KEY"
mc admin policy attach local restic-rw --user restic-backup

MinIO 提供内置的 readwrite 策略。使用它可以少执行一个命令,但它会授予对服务器上所有存储桶的完全访问权限。上面的策略有意将存储桶名称写了两次:一次写作 arn:aws:s3:::restic,用于列出存储桶;另一次写作 arn:aws:s3:::restic/*,用于访问其中的对象。在 S3 中,存储桶和其中的对象是不同的资源,因此策略只指定其中一个时会失败,而且错误表现得像客户端损坏。

在信任该限制之前先进行测试。

mc alias set resticuser http://127.0.0.1:9000 restic-backup "$RESTIC_KEY"
mc ls resticuser/restic
mc ls resticuser/backups

第一个 ls 会成功,第二个会因 Access Denied 失败。未经测试的策略只是猜测。

现在让 restic 使用该存储桶。restic 从标准 AWS 环境变量读取 S3 凭据,因此不需要使用 restic 专用的凭据文件。

sudo apt install -y restic
export AWS_ACCESS_KEY_ID=restic-backup
export AWS_SECRET_ACCESS_KEY="$RESTIC_KEY"
restic -r s3:http://127.0.0.1:9000/restic init
restic -r s3:http://127.0.0.1:9000/restic backup /etc
restic -r s3:http://127.0.0.1:9000/restic snapshots

restic init 会要求输入存储库密码。该密码会加密存储库,因此 MinIO 始终只保存密文;如果密码丢失,备份也会丢失。由 systemd 计时器启动的任务没有终端可供输入,因此应将 RESTIC_PASSWORD_FILE 设置为权限模式为 600 的文件,以便执行计划备份。

有一条部署规则比上面的任何命令都重要。将 restic 存储库放在与受保护数据相同的 VPS 上,只能避免糟糕的 rm,无法避免其他问题。MinIO 节点应位于另一台机器上,最好位于不同的区域。VPS 上的 restic 备份介绍了如何在此基础上配置计划任务和保留策略。

使用 nginx 终止 TLS

MinIO 运行在 localhost 上,因此 nginx 是对外提供服务的入口。先按 使用 certbot 和 nginx 配置 Let's Encrypt 证书 中的说明签发证书,然后使用以下 server 块。

server {
    listen 443 ssl;
    server_name s3.example.com;

    ignore_invalid_headers off;
    client_max_body_size 0;
    proxy_buffering off;
    proxy_request_buffering off;

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 300;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;
        proxy_pass http://127.0.0.1:9000;
    }
}

其中几行配置是必需的。client_max_body_size 0 会取消默认的 1 MB 请求体大小限制;否则,任何更大的上传都会在 MinIO 收到请求之前被 413 Request Entity Too Large 拒绝。proxy_request_buffering off 会直接转发上传内容,因为默认配置会先将整个请求写入临时文件,大型对象会因此占用两份磁盘空间。proxy_set_header Host $http_host 容易被忽略:S3 签名包含 Host 请求头,因此代理如果重写该请求头,每个请求都会因 SignatureDoesNotMatch 失败,即使访问日志显示请求已正常到达。

还要将 MinIO 的公开名称告知它,这样它生成的链接才会指向代理,而不是 localhost。

echo 'MINIO_SERVER_URL=https://s3.example.com' | sudo tee -a /etc/default/minio
sudo systemctl restart minio

防火墙规则应保持精简。允许 SSH 和 HTTPS,不要为端口 9000 和 9001 添加任何规则,因为绑定到 127.0.0.1 的地址无论防火墙如何配置,都无法从其他机器访问。VPS 上的 ufw 防火墙基础包含相关命令。

何时单节点 MinIO 足够,何时应选择真正的 S3

这里的单节点指只有一个磁盘。因此,部署中没有校验块,也没有第二份副本;每个对象的持久性取决于单个 VPS 磁盘的可靠性。MinIO 的官方文档自托管社区文档于 2025 年 10 月下线后,就只作为源文件存在于 minio/docs 仓库中。该文档将独立部署定位为早期开发和评估用途,并明确表示 MinIO 不建议在生产环境中使用。可以为存储桶启用对象锁定,但在单个 VPS 上,它只能阻止通过 S3 API 删除对象。拥有该机器 root 权限的任何人仍可删除磁盘上的文件,因此不要在此配置上向任何人承诺不可变的保留策略。

它适合作为另一个区域中第二台 VPS 上的 restic 目标,也适合作为开发工作和 CI 构建产物的 S3 端点;在这些场景中,丢失存储桶只会导致重新构建,不会造成其他损失。对于小型应用的用户上传功能,这种方案也可以接受,但前提是您负责恢复方案,并且确实测试过恢复流程。

当合同或监管机构要求对象锁定或多区域持久性时,应选择托管 S3。如果您不想在 03:00 因磁盘已满而成为被呼叫处理故障的人,也应选择托管 S3。代码冻结是另一个需要正视的原因。截至 2026 年 10 月 10 日,社区版已归档且不再维护;其最后一个版本是 RELEASE.2025-10-15T17-29-55Z,本指南编译的就是该版本。之后不会再有任何修复,因此运行它就意味着接受这一点,或者迁移到 MinIO 的商业 AIStor 产品线,或其他自托管存储方案;自托管 MinIO 替代方案对此进行了比较。

有一条边界值得明确,因为这个问题经常出现。对象存储不是数据库。每次写入都会替换整个对象,因此在 S3 存储桶中直接保存正在使用的 SQL 文件既慢又不安全。应将数据库保存在本地磁盘,并备份到存储桶中:在 VPS 上运行生产环境 SQLite介绍了这种分工。

故障模式和您将看到的消息

systemctl enable --now 后单元立即失败。 读取 journalctl -u minio -n 30 --no-pager。Failed to load environment files: No such file or directory 表示单元中缺少 /etc/default/minio,或其路径拼写错误。以 permission denied 结尾的消息表示服务帐户无法写入数据目录,因此请检查 stat -c '%U' /var/lib/minio/data 是否输出 minio-user。

minioadmin:minioadmin 仍可登录。 环境文件从未传递到进程。确认单元包含 EnvironmentFile=/etc/default/minio,运行 sudo systemctl daemon-reload,然后重启服务。MinIO 只在启动时读取其 root 凭据,因此编辑该文件但不重启不会产生任何变化。

启动时出现 Address already in use。 另一个进程占用了端口 9000。更改 MinIO 端口前,使用 sudo ss -ltnp | grep :9000 查找该进程。

通过代理上传超过 1 MB 的文件失败。 nginx 返回了 413 Request Entity Too Large,MinIO 从未收到该请求。在 server block 中设置 client_max_body_size 0。

SignatureDoesNotMatch。 密钥不正确,或者客户端与 MinIO 之间的某个组件重写了 Host 标头,而签名涵盖了该标头。

RequestTimeTooSkewed。 客户端或服务器的时钟不正确。每个 S3 请求都包含时间戳,超出 15 分钟的时间窗口后会被拒绝。检查 timedatectl,并确认时间同步处于启用状态。

对您确认存在的存储桶出现 Access Denied。 该密钥的作用域属于其他存储桶。使用 mc admin policy info local restic-rw 输出策略实际允许的内容,并比较资源行中的存储桶名称。

FAQ

单节点 MinIO 足以用于真正的备份吗?

如果将其作为 restic 的目标,并且与受保护数据位于不同的机器上,那么它足够使用。但它不适合作为唯一副本。单磁盘部署没有任何校验数据,因此 MinIO 内部不存在第二份副本。如果该 VPS 磁盘丢失数据,对象也会丢失。请在其他位置保留第二个目标,并至少从两个目标各恢复一次,以确认恢复流程正常。

为什么 sha256sum -c 无法处理 MinIO 的校验和文件?

只有在较早保存的副本中才会遇到该文件,因为在 10 October 2026 检查时,dl.min.io 对每个社区版二进制文件和校验和文件都返回了 410 Gone。该文件中哈希值后的标签用于标识发行版本,例如 minio.RELEASE.2025-09-07T16-13-09Z;而下载的文件通常名为 minio。sha256sum -c 会查找校验和文件中写入的文件名,但找不到该文件,于是报告 No such file or directory 和 WARNING: 1 listed file could not be read。下载本身没有问题。请直接比较哈希字符串并忽略该标签,因为标签不具有安全含义。

MinIO 管理 Web 控制台去哪了?

MinIO 在 May 2025 从社区版控制台中移除了管理功能,只在 Web 界面中保留对象浏览器。现在应使用 mc 客户端管理存储桶和用户,例如使用 mc admin user add 和 mc admin policy attach 等命令。在社区版中,这是预期的操作方式,而不是临时变通方案,因此本指南全部通过命令行完成。

如何将 restic 指向 MinIO 作为 S3 后端?

将 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY 设置为 MinIO 访问密钥及其密钥,然后使用形如 s3:https://s3.example.com/restic 的仓库字符串,其中最后一个路径元素是存储桶名称。请先使用 mc mb 创建存储桶,因为作用域限定为单个存储桶的密钥无权创建存储桶。restic 会在上传前使用自己的仓库密码加密所有内容,因此 MinIO 保存的是密文,无法看到您的文件。

必须让 MinIO 运行在 nginx 后面吗?

只要客户端不在同一台机器上,就必须使用 TLS(传输层安全),因为 S3 凭据和对象数据都会在请求中传输。最简单的方式是在 443 端口运行带有 certbot 证书的代理,这样证书续期也不需要由 MinIO 处理。如果将 --certs-dir 指向包含 public.crt 和 private.key 的目录,MinIO 也可以自行终止 TLS;但此时服务账户必须具有对续期后私钥的读取权限,为获得相同结果需要额外配置。