QNAP NAS 搭建个人网站实战:Docker 部署、SSH 密钥配置、自动同步与 VS Code 远程编辑
一、项目背景
最近,我尝试利用家里的 QNAP NAS 搭建一个个人静态网站,希望能够通过 NAS 提供网页访问服务,并在 MacBook 上方便地修改网页内容。
最初,我采用了比较常见的方案:在 MacBook 上编辑 HTML 文件,再通过 rsync 自动同步到 NAS。
经过测试,网站能够正常显示,文件也能够自动同步。不过,这种方式存在一个明显的问题:MacBook 必须保持开机,而且自动同步脚本需要持续运行。
因此,在完成自动同步方案后,我又决定尝试更直接的方式:通过 Tailscale 远程连接 NAS,使用 VS Code 直接编辑 NAS 上的网站文件。
本文记录整个搭建过程,包括 Docker 部署、SSH 连接、密钥生成、公钥认证、自动同步、问题排查,以及后续远程编辑方案的设计。
本文使用的命令主要针对 macOS 和 QNAP QTS 环境。其他 NAS 设备或系统版本可能需要调整相关配置。
二、硬件与软件环境
本次使用的环境如下。
| 项目 | 配置 |
|---|---|
| NAS | QNAP TS-216-2G |
| NAS 系统 | QTS 5.2.9 |
| Docker Engine | 27.1.2 |
| Docker Compose | 2.29.1 |
| NAS 架构 | ARM64 |
| 客户端 | MacBook |
| Web 服务器 | Nginx |
| 远程网络 | Tailscale |
| 文件传输 | SSH、rsync |
| 文件监控 | fswatch |
| 目标编辑器 | VS Code |
NAS 上原本已经运行 Vaultwarden 和 Nginx Proxy Manager,因此本次部署采用独立的网站目录和独立的 Nginx 容器,不修改已有应用的配置。
为了方便其他读者复现,本文将个人信息统一替换为占位符。
| 占位符 | 含义 |
|---|---|
<NAS_USERNAME> |
NAS 登录用户名 |
<NAS_TAILSCALE_IP> |
NAS 的 Tailscale IP 地址 |
<NAS_LAN_IP> |
NAS 的局域网 IP 地址 |
<NAS_HOSTNAME> |
NAS 的设备名称 |
<SITE_DOMAIN> |
个人网站域名 |
<SITE_DIRECTORY> |
NAS 上的网站目录 |
<LOCAL_SITE_DIRECTORY> |
Mac 本地的网站项目目录 |
<SHARE_NAME> |
QNAP 上配置的 SMB 共享名称 |
注意: 文中的尖括号占位符只是说明用途,并不是可以直接执行的 Shell 参数。执行命令前,必须替换为自己环境中的真实值。
三、在 QNAP 上准备网站目录
首先,在 NAS 上准备一个专门用于存放网页文件的目录。
本次使用的目录结构如下:
css
<SITE_DIRECTORY>/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── main.js
└── images/
如果网站目前只有一个 HTML 文件,也可以先只保留 index.html,以后再逐步添加 CSS、JavaScript 和图片。
QNAP 的实际目录路径可能因存储卷和共享文件夹配置不同而变化,因此不要直接照搬其他设备上的路径。
在本次环境中,网站目录通过 /share/WebSite 访问。对于其他 NAS,应先确认共享文件夹实际映射到哪个路径。
如果目录中已经存在文件,不要为了重新部署网站而直接清空整个目录。
四、使用 Docker 部署 Nginx 静态网站
本次使用 Nginx 提供静态网页服务。
首先准备好 index.html,然后通过 SSH 登录 NAS,在 QNAP 上执行以下命令:
ruby
sudo docker run -d \
--name personal-site \
--restart unless-stopped \
-p 8090:80 \
-v <SITE_DIRECTORY>:/usr/share/nginx/html:ro \
nginx:alpine
执行前,需要将 <SITE_DIRECTORY> 替换为 NAS 上网站目录的真实绝对路径。
1. 参数说明
-d:让容器在后台运行。--name personal-site:将容器命名为personal-site。--restart unless-stopped:除非手动停止容器,否则 Docker 会尝试在适当情况下自动重启它。-p 8090:80:将 NAS 的 8090 端口映射到容器的 80 端口。-v ...:/usr/share/nginx/html:ro:将 NAS 网站目录挂载到 Nginx 默认网页目录,并设置为只读。nginx:alpine:使用较轻量的 Nginx Alpine 镜像。
这里的 :ro 表示容器只能读取网站文件,不能通过该挂载点直接修改文件。但拥有 NAS 目录写入权限的用户仍然可以通过 SMB、SSH 或 NAS 文件管理功能修改网页。
2. 检查容器状态
执行:
ini
sudo docker ps --filter name=personal-site
如果容器正常运行,应当能看到类似的端口映射:
bash
0.0.0.0:8090->80/tcp
随后在浏览器中访问:
arduino
http://<NAS_LAN_IP>:8090
将 <NAS_LAN_IP> 替换为 NAS 的实际局域网 IP 地址。
经过测试,个人主页已经能够正常显示。
至此,静态网站服务部署完成。
3. 为什么使用独立容器
NAS 上原本可能已经运行其他 Docker 应用。为了降低误操作风险,本次网站使用独立的 personal-site 容器。
后续修改网页文件时,不需要重新创建容器,也不需要修改 Vaultwarden 或 Nginx Proxy Manager 的配置。
只要网站文件位于正确的挂载目录中,Nginx 就可以直接读取更新后的内容。
五、通过 Tailscale 远程连接 NAS
为了能够在外出时管理 NAS,本次使用 Tailscale 建立私有网络连接。
NAS 和 MacBook 均已配置 Tailscale,MacBook 可以通过 Tailscale 访问 NAS 的 SSH 服务。
首先获取 NAS 的 Tailscale IP 地址,然后在 Mac 终端执行:
xml
ssh <NAS_USERNAME>@<NAS_TAILSCALE_IP>
例如,假设 NAS 的用户名是 myuser,Tailscale IP 是 100.x.y.z,命令格式就是:
css
ssh myuser@100.x.y.z
这里的地址只是格式示例,不代表任何真实设备地址。
如果首次连接时出现主机身份确认提示,应先确认连接的确实是自己的 NAS,再接受该主机密钥。
连接成功后,就可以通过 SSH 在 NAS 上执行命令。
为什么选择 Tailscale
使用 Tailscale 可以在设备之间建立私有网络连接,不必为了远程管理而直接把 NAS 的 SSH 或 SMB 端口开放到公网。
不过,Tailscale 并不会自动替代 NAS 的账户权限管理。仍然需要正确配置 NAS 用户、SSH 认证和共享文件夹权限。
六、从零开始配置 SSH 公钥认证
为了让 MacBook 能够通过 SSH 安全连接 NAS,并为后续文件传输和远程管理做好准备,需要配置 SSH 密钥认证。
如果电脑上还没有 SSH 密钥,可以从创建密钥对开始。
1. 检查本机是否已有 SSH 密钥
在 Mac 终端执行:
bash
ls -la ~/.ssh
如果提示目录不存在,可以先创建:
bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
如果目录已经存在,则不需要重复创建。
常见的 SSH 密钥文件如下:
javascript
~/.ssh/
├── id_ed25519
└── id_ed25519.pub
其中:
id_ed25519是私钥,只能保存在自己的电脑或受信任的密钥管理环境中。id_ed25519.pub是公钥,可以安装到需要登录的服务器上。
如果已经存在可用密钥,可以选择继续使用;如果没有,或者希望为 NAS 单独创建一组密钥,可以继续下一步。
不要为了执行本教程而随意覆盖已有密钥。 已有密钥可能正在用于其他服务器或 Git 服务。
2. 创建新的 SSH 密钥对
在终端中执行:
arduino
ssh-keygen -t ed25519 -C "qnap-nas-access"
参数说明:
ssh-keygen:SSH 密钥生成工具。-t ed25519:使用 Ed25519 密钥算法。-C "qnap-nas-access":为密钥添加备注,方便以后识别。
执行后,终端会询问密钥保存位置:
vbnet
Enter file in which to save the key
如果希望为 NAS 单独创建一组密钥,可以输入:
javascript
~/.ssh/qnap_ed25519
这样生成的文件名为:
javascript
~/.ssh/qnap_ed25519
~/.ssh/qnap_ed25519.pub
这里的 qnap_ed25519 是通用示例名称,读者也可以根据自己的习惯命名。
接下来,终端会询问是否设置密钥口令(passphrase)。
建议设置一个强口令,以保护本地私钥。之后使用密钥时,可能需要输入该口令,也可以使用 macOS Keychain 辅助管理。
生成完成后,执行:
bash
ls -l ~/.ssh/qnap_ed25519*
确认两个文件都已创建。
3. 将公钥添加到 NAS
创建密钥后,需要将公钥添加到 NAS 的授权密钥列表中。
首先确保能够通过 Tailscale SSH 连接 NAS:
xml
ssh <NAS_USERNAME>@<NAS_TAILSCALE_IP>
如果连接成功,退出 SSH 会话:
bash
exit
随后在 MacBook 上执行:
bash
cat ~/.ssh/qnap_ed25519.pub | \
ssh <NAS_USERNAME>@<NAS_TAILSCALE_IP> \
'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'
执行前,将用户名和 IP 地址占位符替换为实际值。
这条命令会将公钥追加到 NAS 用户主目录下的 authorized_keys 文件中。
其中:
mkdir -p ~/.ssh:确保 SSH 配置目录存在。chmod 700 ~/.ssh:限制 SSH 配置目录的访问权限。cat >> ~/.ssh/authorized_keys:将公钥追加到授权文件,而不是覆盖已有内容。chmod 600 ~/.ssh/authorized_keys:限制授权密钥文件的访问权限。
NAS 账户必须有权限在自己的主目录中创建和修改这些文件。
如果 NAS 不允许该账户修改 .ssh 目录,或者公钥认证仍然失败,应检查 QNAP 的 SSH 配置和账户权限,而不是直接修改整个 NAS 的权限。
4. 测试 SSH 公钥登录
添加公钥后,使用以下命令测试:
ini
ssh -i ~/.ssh/qnap_ed25519 \
-o IdentitiesOnly=yes \
-o PreferredAuthentications=publickey \
-o PasswordAuthentication=no \
<NAS_USERNAME>@<NAS_TAILSCALE_IP>
参数说明:
-i ~/.ssh/qnap_ed25519:指定用于认证的私钥。IdentitiesOnly=yes:要求 SSH 使用指定的身份文件进行认证。PreferredAuthentications=publickey:优先使用公钥认证。PasswordAuthentication=no:本次测试不使用 SSH 密码认证。
如果成功连接 NAS,就说明公钥认证已经生效。
如果出现:
java
Permission denied (publickey)
可以依次检查:
- 是否使用了正确的 NAS 用户名。
- 公钥是否添加到该用户的
authorized_keys文件。 - 测试命令指定的私钥是否与安装到 NAS 的公钥对应。
- NAS 是否允许该账户使用公钥认证。
不要将私钥内容复制到 NAS 的 authorized_keys 文件中。该文件应当存放公钥内容。
5. 使用 macOS Keychain 管理私钥口令
如果创建密钥时设置了口令,每次使用密钥时可能需要输入口令。
macOS 可以通过 SSH Agent 和 Keychain 辅助管理。
对于上面创建的密钥,可以执行:
javascript
ssh-add --apple-use-keychain ~/.ssh/qnap_ed25519
首次添加时,根据提示输入私钥口令。
然后执行:
csharp
ssh-add -l
查看当前 SSH Agent 中已加载的密钥。
具体行为可能因 macOS 版本和 SSH 配置而有所不同。如果命令提示不支持某个选项,应根据当前系统版本调整配置。
6. 使用 SSH 配置文件简化连接
如果不希望每次输入完整的用户名、IP 地址和密钥路径,可以编辑 MacBook 上的 SSH 配置文件。
如果文件不存在,先执行:
bash
touch ~/.ssh/config
chmod 600 ~/.ssh/config
然后在 ~/.ssh/config 中添加:
bash
Host qnap-nas
HostName <NAS_TAILSCALE_IP>
User <NAS_USERNAME>
IdentityFile ~/.ssh/qnap_ed25519
IdentitiesOnly yes
保存后,可以使用简化命令连接:
ssh qnap-nas
后续使用 rsync 时,也可以通过 qnap-nas 这个主机别名连接 NAS。
7. SSH 密钥安全说明
SSH 密钥由私钥和公钥组成,两者的用途不同。
| 文件 | 用途 | 是否可以公开 |
|---|---|---|
qnap_ed25519 |
私钥,用于证明客户端身份 | 不可以 |
qnap_ed25519.pub |
公钥,安装到 NAS 上用于验证客户端 | 可以按需分发 |
不要将私钥上传到公开代码仓库、发送给他人或放进博客文章。
如果怀疑私钥已经泄露,应及时撤销 NAS 上对应的公钥授权,并生成新的密钥对。
七、第一种方案:使用 rsync 自动同步网页
在完成网站部署后,我最初采用了本地编辑、远程同步的方式。
MacBook 上的网页目录表示为:
xml
<LOCAL_SITE_DIRECTORY>
NAS 上的目标目录表示为:
xml
<SITE_DIRECTORY>
整个流程如下:
markdown
MacBook 编辑网页
↓
fswatch 监控文件变化
↓
rsync 通过 SSH 传输文件
↓
QNAP 更新网站目录
↓
Nginx 提供网页访问
1. 安装 fswatch
fswatch 用于监控文件变化。
如果尚未安装,可以通过 Homebrew 安装:
brew install fswatch
如果已经安装,则不需要重复执行。
2. 测试 rsync 连接
首先进行一次模拟传输:
ini
rsync -avn \
--exclude='.DS_Store' \
-e ssh \
<LOCAL_SITE_DIRECTORY>/ \
qnap-nas:<SITE_DIRECTORY>/
这里使用了前面配置的 qnap-nas SSH 主机别名。
参数说明:
-a:归档模式。-v:显示传输过程。-n:模拟运行,不实际修改目标文件。--exclude='.DS_Store':忽略 macOS 自动生成的.DS_Store文件。-e ssh:使用 SSH 作为传输通道。
源目录末尾的 / 表示同步目录中的内容,而不是将整个目录作为一个子目录复制过去。
如果没有配置 SSH 主机别名,也可以使用完整形式:
xml
rsync -avn \
--exclude='.DS_Store' \
-e ssh \
<LOCAL_SITE_DIRECTORY>/ \
<NAS_USERNAME>@<NAS_TAILSCALE_IP>:<SITE_DIRECTORY>/
3. 处理 QNAP 权限问题
最初使用 rsync -av 时,出现过类似错误:
vbnet
rsync: failed to set times on "...": Operation not permitted
随后又出现:
vbnet
rsync: failed to set permissions on "...": Operation not permitted
原因是 rsync 默认会尝试保留部分目录时间和权限信息,而当前 NAS 账户对目标目录的相关属性没有足够的修改权限。
本次没有为了绕过问题而直接使用管理员账户或修改整个目录的权限,而是调整 rsync 参数。
最终使用:
css
rsync -rv \
--omit-dir-times \
--no-perms \
--exclude='.DS_Store' \
-e ssh \
<LOCAL_SITE_DIRECTORY>/ \
qnap-nas:<SITE_DIRECTORY>/
参数说明:
-r:递归同步目录。--omit-dir-times:不尝试保留目录的修改时间。--no-perms:不要求目标端保留源文件的权限设置。
这样可以避免 rsync 因为无法修改目标目录的部分属性而报错。
4. 编写自动同步脚本
在 MacBook 上创建脚本文件:
bash
~/site-sync.sh
脚本内容如下:
perl
#!/bin/bash
set -u
SOURCE="$HOME/<LOCAL_SITE_FOLDER_NAME>/"
DEST="qnap-nas:<SITE_DIRECTORY>/"
sync_site() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 正在同步网站..."
if rsync -rv \
--omit-dir-times \
--no-perms \
--exclude='.DS_Store' \
-e "ssh -o BatchMode=yes -o ConnectTimeout=10" \
"$SOURCE" "$DEST"; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 同步成功"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 同步失败;请检查网络和 SSH 密钥"
fi
}
# 启动时先同步一次
sync_site
# 监控本地文件变化;变化后等待两秒,再同步
fswatch -0 "$SOURCE" | while IFS= read -r -d '' event; do
sleep 2
sync_site
done
这里的 <LOCAL_SITE_FOLDER_NAME> 和 <SITE_DIRECTORY> 是占位符,执行前需要替换为真实目录名称和路径。
如果使用 SSH 配置文件中的主机别名 qnap-nas,脚本会通过该别名连接 NAS。若使用专门创建的密钥,应确保 SSH 配置文件中的 IdentityFile 指向正确的私钥。
这个脚本首先执行一次同步,然后持续监控本地目录。当检测到文件变化时,它会等待两秒,再运行 rsync,以减少连续保存文件时频繁传输的情况。
脚本使用 BatchMode=yes,因此 SSH 认证失败时不会停下来等待交互式密码输入。公钥认证应当提前测试成功。
5. 赋予执行权限并运行
执行:
bash
chmod +x ~/site-sync.sh
然后启动:
bash
~/site-sync.sh
如果出现:
erlang
正在同步网站...
同步成功
说明脚本已成功完成一次同步。
脚本启动后会继续监控文件变化,因此运行期间不要关闭它。
6. 验证自动同步
在 MacBook 上修改并保存本地的 index.html。
如果终端随后自动出现新的同步日志,就说明文件监控已经触发。
在 NAS 上检查文件:
arduino
ssh qnap-nas \
'ls -l <SITE_DIRECTORY>/index.html'
本次测试中,修改网页后终端能够自动显示"同步成功",随后 NAS 上的网页也更新成功。
有时浏览器仍会短暂显示旧内容。这种情况下,可以尝试在 Chrome 中按:
Command + Shift + R
强制刷新页面。
如果仍然存在延迟,应分别检查 NAS 文件是否更新,以及浏览器请求是否命中了正确的网站服务,不应直接认定是 rsync 失败。
八、为什么后来决定调整自动同步方案
虽然 fswatch + rsync 已经测试成功,但实际使用时发现了一个问题:
自动同步脚本必须持续运行,才能监控本地文件。
这意味着:
- MacBook 需要处于开机状态。
- 监控脚本不能退出。
- MacBook 睡眠、断网或者 SSH 连接失败时,自动同步可能停止或失败。
- 如果换一台电脑编辑网页,还需要重新配置相应的同步环境。
因此,这种方案虽然适合本地开发,但对于希望随时远程管理 NAS 的使用场景,并不是最省心的选择。
最终,我决定尝试另一种方式:直接通过网络共享访问 NAS 上的网站目录,再用 VS Code 编辑文件。
九、第二种方案:SMB + Tailscale + VS Code 直接编辑
新方案的目标是:
css
MacBook 上的 VS Code
↓
SMB 文件共享
↓
Tailscale 私有网络
↓
QNAP 网站目录
↓
Nginx 网站
这样,VS Code 保存文件时,修改会直接写入 NAS 的共享目录,不需要再经过本地同步脚本。
1. 检查 QNAP 共享文件夹
在 QTS 管理界面中,进入:
控制台
→ 权限
→ 共享文件夹
确认以下条件:
- 已经存在网站使用的共享文件夹。
- SMB 文件服务已启用。
- 用于远程编辑的 NAS 用户具有读写权限。
如果共享文件夹已经存在,不需要重新创建。
2. 在 MacBook 上连接 SMB 共享
打开 Finder,按:
Command + K
输入服务器地址:
xml
smb://<NAS_TAILSCALE_IP>/<SHARE_NAME>
其中:
<NAS_TAILSCALE_IP>是 NAS 的实际 Tailscale IP。<SHARE_NAME>是 QNAP 上实际配置的网站共享名称。
点击连接。
如果出现身份验证提示,选择注册用户,并输入 NAS 用户名和对应密码。
连接成功后,Finder 会显示已挂载的共享目录。
注意,共享名称不一定与 NAS 内部的绝对路径相同。SMB 会将共享名称映射到 QNAP 上对应的实际目录。
3. 在 VS Code 中打开共享目录
打开 VS Code,选择:
arduino
File → Open Folder...
如果共享目录挂载到了 /Volumes 下,就可以打开对应的目录,例如:
xml
/Volumes/<SHARE_NAME>
如果实际挂载名称不同,可以在 Mac 终端中执行:
bash
ls /Volumes
找到实际目录后,再在 VS Code 中打开它。
随后就可以直接编辑 index.html。
4. 验证修改是否直接写入 NAS
在 VS Code 中修改网页内容并保存,再执行:
arduino
ssh qnap-nas \
'ls -l <SITE_DIRECTORY>/index.html'
检查文件的修改时间和大小是否发生变化。
然后刷新网站页面,确认修改是否生效。
需要说明:SMB + VS Code 直接编辑是本项目的目标方案,在本文整理时仍需完成最终的挂载、编辑和保存验证。 在确认测试成功前,不应将这一部分描述成已经完成的部署。
另外,SMB 直接编辑依赖 MacBook 能够连接 NAS。外出时需要 Tailscale 正常运行;不要为了方便而将 SMB 的 445 端口直接开放到公网。
十、如何停止旧的自动同步任务
切换到直接编辑方案后,就不再需要原来的自动同步脚本。
1. 停止正在运行的脚本
如果脚本仍在终端中运行,按:
Control + C
停止监控。
2. 检查是否配置过自动启动
执行:
javascript
ls ~/Library/LaunchAgents
检查是否存在此前为网站同步配置的 LaunchAgent 文件。
如果存在相关文件,应先确认它对应的是哪个任务,再有针对性地停用,不要直接删除所有 LaunchAgent 文件。
本次检查时,没有发现对应的自动启动任务文件,因此不需要停用相应任务。
3. 删除旧脚本(可选)
确认脚本已经停止,而且不再需要自动同步后,可以删除:
bash
rm ~/site-sync.sh
这只会删除 MacBook 上的同步脚本,不会删除 NAS 上的网站文件,也不会删除本地网页源文件。
建议暂时保留本地网页目录,作为备份,避免后续误操作导致网页内容丢失。
十一、安全注意事项
1. 不要随意修改现有容器
本次新建的网站容器名为:
personal-site
它与 NAS 上其他应用的容器相互独立。
后续维护个人网站时,不需要为了修改网页而删除或重建其他已有容器。
2. 不要将 SSH 或 SMB 端口直接暴露到公网
本次通过 Tailscale 访问 NAS。
建议继续使用私有网络连接,而不是在路由器上直接转发 SSH 的 22 端口或 SMB 的 445 端口。
3. 注意网站目录权限
Nginx 容器使用只读挂载:
ruby
<SITE_DIRECTORY>:/usr/share/nginx/html:ro
这可以减少容器对网站文件的意外修改,但并不能替代 NAS 文件权限和账户安全管理。
如果需要直接通过 SMB 编辑,应该只授予实际需要的目录写入权限,不应为了方便而将整个 NAS 的目录权限全部放开。
4. 保留备份
自动同步不是备份。
如果本地文件被误删、错误内容被同步,或者直接编辑时覆盖了正确版本,单纯依靠同步并不能恢复旧内容。
因此,正式长期使用前,应保留独立的本地副本,并为网站目录建立合适的版本管理或备份机制。
十二、总结
这次搭建过程让我认识到,NAS 网站部署并不只是让 Nginx 能够显示 HTML 文件,还需要考虑远程访问、文件编辑、权限控制以及长期维护方式。
最初采用的方案是:
MacBook + fswatch + rsync + SSH + Tailscale
这个方案已经成功实现网页文件自动同步,但需要 MacBook 持续运行监控脚本。
随后,我决定尝试更直接的方式:
css
MacBook + VS Code + SMB + Tailscale + QNAP
如果完成挂载和保存测试,这种方式就可以省去持续运行的同步脚本,直接编辑 NAS 上的网页文件。
对于已经拥有 QNAP、Tailscale 和 Docker 环境的用户来说,可以先部署独立的 Nginx 容器,再根据实际工作习惯选择本地同步或远程编辑。
最重要的是:不要为了部署一个简单的个人网站,破坏 NAS 上已经稳定运行的其他服务。 尽量复用现有网络和文件服务,独立管理网站目录与容器,才能让后续维护更简单、安全。