Ubuntu 24.04 部署 MinIO 自建 S3 对象存储
在一台 Ubuntu 24.04 VPS 上部署 MinIO:验证二进制文件、配置 systemd、使用 mc 管理存储桶和密钥、生成预签名 URL,并将 restic 指向自有 S3 端点。
使用 MinIO 自托管对象存储的优势
MinIO 是支持 Amazon S3 API 的自托管对象存储。将 restic 或任何 S3 SDK 指向您自己的服务器,只需修改一个端点设置,客户端就无法区分两者。本指南将在 Ubuntu 24.04 上构建单节点部署:使用经过验证的二进制文件、专用系统用户,以及一个将 root 凭据保留在单元文件之外的 systemd 单元;最后创建一个供 restic 备份数据的存储桶。
S3(简单存储服务)是 HTTP API,而不是文件系统。您可以使用键将对象 PUT 到存储桶中,再将其 GET 出来;S3 不支持部分写入或重命名。备份工具适合这种模型,因为对象要么完整到达,要么未到达。
单个节点只保存一份数据副本。这就是您需要承担的取舍。您可以以 VPS 的成本获得一个由自己控制的 S3 端点,同时也需要自行承担云服务商原本负责的所有工作,包括更换故障磁盘和修补服务器软件。本指南接近末尾的部分会明确说明这种取舍在什么情况下是合理的。
2026年7月 MinIO 社区版的状态
在基于此内容进行部署前,请先阅读本节,因为近期情况发生了变化。2025年5月,MinIO 从社区版的 Web 控制台中移除了管理功能。浏览器中保留的只是对象浏览器,因此现在应改用 mc 命令行客户端管理存储桶和访问密钥。
2025年晚些时候,MinIO 停止发布预编译的社区版二进制文件。项目 README 现在说明,社区版仅以源代码形式分发。旧的下载 URL 仍然有效:截至2026年7月,它们提供服务器构建版本 RELEASE.2025-09-07T16-13-09Z 和客户端构建版本 RELEASE.2025-08-13T08-35-41Z,并且尚未出现更新的社区版构建。因此,下面的二进制文件确实存在且可以运行,但其版本已冻结。2025年9月之后发布的安全修复不包含在其中。
这一事实决定了本指南其余部分的配置方式。这就是为什么本指南中的 MinIO 监听 127.0.0.1,并且只能通过您控制的代理访问互联网。如果您希望持续获取修复,请从源代码构建。供应商 README 提供了一个命令 go install github.com/minio/minio@latest,该命令需要 Go 工具链,并将二进制文件写入 ~/go/bin/minio。将该二进制文件安装到 /usr/local/bin/minio,此处的其他步骤均无需更改。
安装 MinIO 二进制文件并验证下载
下载指定版本及其发布的校验和。-f 标志会让 curl 在发生 HTTP 错误时失败,而不是将错误页面保存为您指定的文件名。否则,您可能会安装一个 404 页面,然后疑惑它为什么无法执行。
cd /tmp
REL=RELEASE.2025-09-07T16-13-09Z
curl -fsSL "https://dl.min.io/server/minio/release/linux-amd64/archive/minio.$REL" -o minio
curl -fsSL "https://dl.min.io/server/minio/release/linux-amd64/archive/minio.$REL.sha256sum" -o minio.sha256sum比较这两个哈希值,并且只比较哈希值。
published=$(awk '{print $1}' minio.sha256sum)
downloaded=$(sha256sum minio | awk '{print $1}')
[ "$published" = "$downloaded" ] && echo "checksum ok"这里不要使用 sha256sum -c minio.sha256sum。该文件中哈希值后写入的标签是 minio.RELEASE.2025-09-07T16-13-09Z,而我们将下载内容保存为 minio,因此 -c 会查找一个不存在的文件。它会报告 No such file or directory,然后报告 WARNING: 1 listed file could not be read。这看起来像是下载已损坏,但实际并非如此。标签只是名称。哈希值才是提供完整性保证的部分。
请明确此检查能够证明什么。二进制文件和哈希值来自同一供应商,并通过同一连接获取。因此,匹配只能证明下载内容完整,且在传输过程中未损坏或被篡改。它不能证明供应商可信。这是另一个问题,没有任何 sha256sum 命令可以解决。
sudo install -o root -g root -m 755 minio /usr/local/bin/minio
minio --versionminio --version 会输出 minio version RELEASE.2025-09-07T16-13-09Z,后面跟着几行构建信息。这里出现 Permission denied 表示模式不正确,出现 command not found 表示 /usr/local/bin 不在您的 PATH 中。
创建系统用户和数据目录
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/miniotee 会截断现有文件,而不是重新创建文件,因此文件模式保持为 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.targetEnvironmentFile 前没有前导 -,这是有意的,不是拼写错误。使用连字符时,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/liveis-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 无关的文件管理器)。
cd /tmp
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o mc
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc.sha256sum -o mc.sha256sum
[ "$(awk '{print $1}' mc.sha256sum)" = "$(sha256sum mc | awk '{print $1}')" ] && echo "checksum ok"
sudo install -o root -g root -m 755 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.txtmc 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-backupMinIO 提供内置的 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 snapshotsrestic 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
这里的单节点指使用一个磁盘且完全没有校验盘。MinIO 官方文档将这种布局定位为适合测试和不要求可用性的小型工作负载。该部署中没有第二份副本,因此每个对象的持久性取决于一块 VPS 磁盘的持久性。依赖分布式纠删码后端的功能(包括存储桶复制和对象锁定)属于多磁盘部署,因此不要在此配置上向任何人承诺不可变的保留策略。
如果将其作为另一个区域中第二台 VPS 上的 restic 目标,或作为开发工作和 CI 构建产物的 S3 端点,它很合适。此时丢失一个存储桶只意味着重新构建,不会造成其他损失。对于小型应用中的用户上传,它也可以胜任,前提是您负责恢复计划,并且确实测试过恢复操作。
如果合同或监管机构要求对象锁定或多区域持久性,或者您不想在 03:00 因磁盘已满而收到告警,请选择托管 S3。构建版本冻结也是一个应如实考虑的原因。截至 July 2026,预编译的社区二进制文件仍来自 September 2025,且不会再获得修复。因此,运行它就意味着接受这一点;另一种选择是从源代码构建,并自行跟进项目更新。
有一个边界值得说明,因为这个问题经常出现。对象存储不是数据库。每次写入都会替换整个对象,因此将实时 SQL 文件放在 S3 存储桶中既慢又不安全。应将数据库保存在本地磁盘,然后备份到存储桶中:在 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 足够使用。但它不适合作为唯一副本。单磁盘部署没有任何校验冗余,因此 MinIO 内部没有第二份副本。如果该 VPS 磁盘丢失数据,对象也会丢失。在其他位置保留第二个目标,并至少从两个目标各恢复一次,以确认恢复流程正常。
为什么 sha256sum -c 在 MinIO 的校验和文件上失败?
因为该文件中哈希值后面的标签是发布版本名称 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 在 2025 年 5 月从社区版控制台中移除了管理功能,只在 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 保存的是密文,无法看到您的文件。
必须在 nginx 后运行 MinIO 吗?
只要客户端不在同一台机器上,就需要 TLS(传输层安全),因为 S3 凭据和对象数据都会通过请求传输。最简单的方式是使用端口 443 上的代理,并配合 certbot 签发的证书。这样可以将证书续期工作与 MinIO 分离。如果将 --certs-dir 指向包含 public.crt 和 private.key 的目录,MinIO 也可以自行终止 TLS。但这样服务账户需要读取续期后的私钥,因此为实现相同结果需要额外配置。