我的VPS服务部署记录

本文记录了在 VPS 服务器上部署各种服务的详细步骤,包括服务器初始化、Docker 环境配置、以及多个自托管服务的部署过程。

我的 VPS 使用的是 AlmaLinux 9 操作系统,所以以下操作都是基于 AlmaLinux 系统。

服务器设置

以下为初次登录 VPS 后的基础配置。AlmaLinux 9 默认开启 SELinux,使用 Nginx 反向代理时需在安装 Nginx 后执行后文中的 setsebool 命令。

设置主机名(可选)

Terminal window
hostnamectl set-hostname vps

配置 AlmaLinux 9 源(可选):先清理默认多份 .repo,再创建一份精简源文件(仅 BaseOS + AppStream)。

第一步:清理默认 yum 源文件

Terminal window
mkdir -p /etc/yum.repos.d/bak
mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/bak/

第二步:新建精简 repo 文件

按机房位置选用下面一段,将 REPO_BASE 替换为对应域名后,整段复制执行(默认官方源、美国 AWS、国内阿里云三选一):

  • 默认官方源(适合美国等,Fastly CDN 加速):REPO_BASE=https://repo.almalinux.org
  • 美国 AWS:REPO_BASE=https://bos.aws.repo.almalinux.org(丹佛用 den 替换 bos
  • 国内阿里云:REPO_BASE=https://mirrors.aliyun.com
Terminal window
REPO_BASE=https://repo.almalinux.org
cat > /etc/yum.repos.d/almalinux.repo << EOF
[baseos]
name=AlmaLinux 9 - BaseOS
baseurl=$REPO_BASE/almalinux/9/BaseOS/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9
[appstream]
name=AlmaLinux 9 - AppStream
baseurl=$REPO_BASE/almalinux/9/AppStream/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9
EOF

清理缓存并重建缓存:

Terminal window
dnf clean all
dnf makecache

更新系统:

Terminal window
dnf update -y

安装常用软件:

Terminal window
dnf install git vim jq chrony -y

设置时钟同步(AlmaLinux 9 默认使用 chrony):

Terminal window
systemctl enable chronyd
systemctl start chronyd
chronyc makestep

设置时区为上海时区:

Terminal window
# 查看当前时区
timedatectl
# 设置时区为上海(Asia/Shanghai)
timedatectl set-timezone Asia/Shanghai
# 验证时区设置
timedatectl
date

配置 git:

Terminal window
git config --global user.name chensoul
git config --global user.email ichensoul@gmail.com

[可选] 使普通用户拥有 root 级 sudo 权限

将用户加入 wheel 组即可获得等同 root 的 sudo 权限(AlmaLinux 9 默认配置为 %wheel ALL=(ALL) ALL):

Terminal window
# 新建用户(若已有用户可只执行 usermod 与 passwd)
useradd -m -s /bin/bash chensoul
usermod -aG wheel chensoul
passwd chensoul
# 验证:切换为该用户后执行,应看到 (ALL) ALL 且 sudo whoami 输出 root
su - chensoul
sudo -l
sudo whoami # 应输出 root

之后可用 su - chensoul 或 SSH 登录该用户,建议配置好 SSH 密钥后再考虑禁用 root 密码登录。

[可选] 设置系统 Swap 交换分区

因为 vps 服务器的运行内存很小,所以这里先设置下 Swap 空间为 4G。

Terminal window
sudo fallocate -l 4G /swapfile && \
sudo dd if=/dev/zero of=/swapfile bs=1024 count=4194304 && \
sudo chmod 600 /swapfile && \
sudo mkswap /swapfile && \
sudo swapon /swapfile && \
echo "/swapfile swap swap defaults 0 0" | sudo tee -a /etc/fstab && \
sudo swapon --show && \
sudo free -h

[可选] 安全加固

在确认已有可用的 SSH 密钥登录或普通用户可 sudo 之前,不要禁用密码登录或 root 登录,以免被锁在机器外。

  • SSH 密钥生成与上传(本地执行)

本机生成密钥对,并将公钥上传到 VPS,以便后续使用密钥登录。

Terminal window
# 生成 Ed25519 密钥对(推荐,比 RSA 更短更安全)
ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519_vps
# 提示输入 passphrase 可留空,或设置后每次使用密钥时需输入

上传公钥到 VPS(任选一种方式):

Terminal window
# 方式一:ssh-copy-id(需当前能密码登录 VPS)
ssh-copy-id -i ~/.ssh/id_ed25519_vps.pub root@your_vps_ip
# 若登录用户为普通用户:ssh-copy-id -i ~/.ssh/id_ed25519_vps.pub user@your_vps_ip
Terminal window
# 方式二:手动追加(将本机公钥内容追加到 VPS 的 authorized_keys)
# 本机查看公钥:cat ~/.ssh/id_ed25519_vps.pub
# 在 VPS 上执行:mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "粘贴公钥内容" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys

上传后务必先测试密钥登录是否成功(新开一个终端窗口,密码登录不要关),再在服务端禁用密码登录:

Terminal window
ssh -i ~/.ssh/id_ed25519_vps root@your_vps_ip
# 若使用非默认密钥路径,可在 ~/.ssh/config 中配置 Host 与 IdentityFile
  • SSH 仅允许密钥登录:确认密钥登录无误后,在服务端用命令修改 /etc/ssh/sshd_config 并重启:
Terminal window
# 备份并设置:禁用密码登录、启用公钥登录
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/; s/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
grep -q '^PasswordAuthentication ' /etc/ssh/sshd_config || echo 'PasswordAuthentication no' | sudo tee -a /etc/ssh/sshd_config
grep -q '^PubkeyAuthentication ' /etc/ssh/sshd_config || echo 'PubkeyAuthentication yes' | sudo tee -a /etc/ssh/sshd_config
sudo systemctl restart sshd
  • 禁用 root 密码登录(仅允许 root 密钥或改用普通用户):确认密钥或普通用户可用后,用命令二选一:
Terminal window
# 仅允许 root 使用密钥登录(推荐)
sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
grep -q '^PermitRootLogin ' /etc/ssh/sshd_config || echo 'PermitRootLogin prohibit-password' | sudo tee -a /etc/ssh/sshd_config
sudo systemctl restart sshd
Terminal window
# 禁止 root 登录,仅用普通用户 + sudo
sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
grep -q '^PermitRootLogin ' /etc/ssh/sshd_config || echo 'PermitRootLogin no' | sudo tee -a /etc/ssh/sshd_config
sudo systemctl restart sshd
  • 安装 fail2ban 防暴力破解
Terminal window
dnf install epel-release -y
dnf install fail2ban fail2ban-firewalld -y
systemctl enable fail2ban --now
# 默认会保护 sshd,可查看状态:fail2ban-client status sshd

安装 Nginx

AlmaLinux 9 默认仓库不含 Nginx 或版本较旧,需要额外添加 Nginx 官方 yum 源,再用 dnf install nginx 安装(本文采用包管理安装,不从源码编译)。

Terminal window
# 添加 Nginx 官方 yum 源(适用于 RHEL 9 / AlmaLinux 9)
cat > /etc/yum.repos.d/nginx.repo << 'EOF'
[nginx-stable]
name=nginx stable repo
baseurl=https://nginx.org/packages/rhel/9/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF
# 若系统已装 AlmaLinux 自带的 nginx-core,先卸载以免与官方 nginx 包冲突
dnf remove -y nginx-core nginx-filesystem nginx-mod-* 2>/dev/null || true
dnf install nginx -y
systemctl enable nginx
systemctl start nginx

打开防火墙,开放 443 端口:

Terminal window
dnf install firewalld -y
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --reload

使用反向代理需要打开 SELinux 网络访问权限:

Terminal window
setsebool -P httpd_can_network_connect on

安装并生成证书

域名托管在 CloudFlare,使用 acme.sh 通过 DNS API 自动申请和续期 SSL 证书。参考 acme.sh DNS API 文档

准备工作

  1. 在 CloudFlare 获取 API Token(推荐)或 Global API Key;使用 Token 时建议同时设置 Account ID
  2. 创建 SSL 证书存储目录
Terminal window
# 邮箱统一使用变量(Git、acme.sh 注册与 CloudFlare 共用)
export GIT_EMAIL=ichensoul@gmail.com
# 创建 SSL 证书目录
mkdir -p /etc/nginx/ssl
# 安装 acme.sh
curl https://get.acme.sh | sh -s email=$GIT_EMAIL
# 使用 API Token(推荐)+ Account ID
export CF_Token="your_api_token"
export CF_Account_ID="your_account_id"
# 或使用 Global API Key:export CF_Key=xxx(邮箱会使用上面的 GIT_EMAIL)
# 申请证书(支持通配符域名)
~/.acme.sh/acme.sh --issue --server letsencrypt --dns dns_cf -d chensoul.cc -d '*.chensoul.cc'
# 复制证书到 Nginx 目录
cp ~/.acme.sh/chensoul.cc_ecc/{chensoul.cc.cer,chensoul.cc.key,fullchain.cer,ca.cer} /etc/nginx/ssl/
# 安装证书并设置自动续期
~/.acme.sh/acme.sh --installcert -d chensoul.cc -d *.chensoul.cc \
--cert-file /etc/nginx/ssl/chensoul.cc.cer \
--key-file /etc/nginx/ssl/chensoul.cc.key \
--fullchain-file /etc/nginx/ssl/fullchain.cer \
--ca-file /etc/nginx/ssl/ca.cer \
--reloadcmd "nginx -s reload"

注意:证书会自动续期,续期命令已添加到 crontab 中。

Docker 安装和配置

一键安装docker和docker compose:

Terminal window
bash <(curl -sSL https://linuxmirrors.cn/docker.sh)

启动 Docker 服务:

Terminal window
systemctl enable docker
systemctl start docker
# 验证安装
docker --version
docker ps

查看docker配置文件,默认会给我们配置刚才选择的镜像加速地址

Terminal window
cat /etc/docker/daemon.json

设置 iptables 允许流量转发:

Terminal window
iptables -P FORWARD ACCEPT

参考 Best Practice: Use a Docker network,创建一个自定义的网络:

Terminal window
docker network create vps

查看 docker 网络:

Terminal window
docker network ls
NETWORK ID NAME DRIVER SCOPE
68f4aeaa57bd bridge bridge local
6a96b9d8617e vps bridge local
4a8679e35f4d host host local
ba21bef23b04 none null local

注意:bridge、host、none 是内部预先创建的网络。

是否需要暴露端口:需要。Nginx 装在宿主机上,通过 proxy_pass http://127.0.0.1:端口 反代到容器,因此各服务的 compose 里要写 ports 映射到宿主机。为减少误暴露,建议只绑定本机:写成 127.0.0.1:3003:3000 而不是 3003:3000,这样外网无法直连该端口;防火墙只放行 80/443 即可,其余端口仅本机访问。

Docker 服务部署

Postgres

PostgreSQL 是一个功能强大的开源关系型数据库管理系统,作为其他服务的数据存储后端。

创建 postgres.yaml 文件:

services:
postgres:
image: postgres:18
restart: always
ports:
- "5435:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: vps@027!
TZ: Asia/Shanghai
healthcheck:
test:
["CMD", "pg_isready", "-U", "postgres", "-d", "postgres"]
interval: 5s
timeout: 10s
retries: 5
networks:
- vps
networks:
vps:
external: true

启动服务:

Terminal window
docker compose -f postgres.yaml up -d
# 查看日志
docker logs -f postgres
# 验证数据库连接
docker exec -it postgres psql -U postgres -d postgres

如果开启了防火墙,开放 5432 端口:

Terminal window
sudo firewall-cmd --permanent --add-port=5432/tcp
sudo firewall-cmd --reload

Umami

Umami 是一个简单、快速、注重隐私的网站分析工具,可以作为 Google Analytics 的开源替代品。

  1. 在 postgres 容器创建 umami 数据库:
Terminal window
docker exec postgres psql -U postgres -c "CREATE DATABASE umami OWNER postgres;"
  1. 通过 docker compose 安装,创建 umami.yaml
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
ports:
- "3003:3000"
environment:
DATABASE_URL: postgresql://postgres:vps@027!@postgres:5432/umami
DATABASE_TYPE: postgresql
HASH_SALT: vps@2025
TRACKER_SCRIPT_NAME: random-string.js
TZ: "Asia/Shanghai"
GENERIC_TIMEZONE: "Asia/Shanghai"
networks:
- vps
restart: always
networks:
vps:
external: true

参考 https://eallion.com/umami/,Umami 的默认跟踪代码是被大多数的广告插件屏蔽的,被屏蔽了你就统计不到访客信息了。如果需要反屏蔽,需要在 docker compose.yml 文件中添加环境变量:TRACKER_SCRIPT_NAME,如:

Terminal window
environment:
TRACKER_SCRIPT_NAME: random-string.js

然后获取到的跟踪代码的 src 会变成:

srcipt.js => random-string.js

启动:

Terminal window
docker compose -f umami.yaml up -d
docker logs -f umami
  1. 设置自定义域名:umami.chensoul.cc

  2. 配置 nginx 配置文件 /etc/nginx/conf.d/umami.conf

server {
listen 80;
listen [::]:80;
server_name umami.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name umami.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:3003;
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;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
expires 12h;
}
}
  1. 重新加载 Nginx 配置并访问服务:
Terminal window
nginx -t
nginx -s reload

访问 https://umami.chensoul.cc/,默认用户名和密码为 admin/umami。登录之后,请立即修改密码,并添加要统计的网站。

Memos

Memos 是一个开源的、自托管的备忘录和知识管理系统,支持 Markdown、标签、搜索等功能。

  1. 在 postgres 容器创建 memos 数据库:
Terminal window
docker exec postgres psql -U postgres -c "CREATE DATABASE memos OWNER postgres;"
  1. 通过 docker compose 安装,创建 memos.yaml
services:
memos:
image: neosmemo/memos:0.18.2
container_name: memos
environment:
- MEMOS_DRIVER=postgres
- MEMOS_DSN=postgres://postgres:vps@027!@postgres:5432/memos?sslmode=disable
volumes:
- ~/.memos/:/var/opt/memos
ports:
- '5230:5230'
networks:
- vps
networks:
vps:
external: true

版本说明

  • 0.21.0 版本:支持 Mermaid、分享功能
  • 0.22.0 版本:去掉了分享功能
  • 0.24.0 版本:支持 Shortcuts
  • 0.24.1 版本:改变了布局
  1. 启动服务:
Terminal window
docker compose -f memos.yaml up -d
docker logs -f memos
  1. 配置 nginx 配置文件 /etc/nginx/conf.d/memos.conf
server {
listen 80;
listen [::]:80;
server_name memos.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name memos.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:5230;
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;
}
}

重新加载 Nginx 配置并访问服务:

Terminal window
nginx -s reload

访问 https://memos.chensoul.cc/ ,默认用户名和密码为 admin/memos。登录之后,请立即修改密码。

N8n

N8n 是一个开源的工作流自动化工具,可以通过可视化界面创建自动化工作流,连接各种服务和 API。

  1. 在 postgres 容器创建 n8n 数据库:
Terminal window
docker exec postgres psql -U postgres -c "CREATE DATABASE n8n OWNER postgres;"
  1. 通过 docker compose 安装,创建 n8n.yaml
services:
n8n:
image: n8nio/n8n
container_name: n8n
restart: always
environment:
- NODE_ENV=production
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=postgres
- DB_POSTGRESDB_PASSWORD=vps@027!
- TZ="Asia/Shanghai"
- GENERIC_TIMEZONE="Asia/Shanghai"
- N8N_DEFAULT_LOCALE=zh-CN
# 是否启用诊断和个性化推荐
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_PERSONALIZATION_ENABLED=false
# 是否在启动时重新安装缺失的包
- N8N_REINSTALL_MISSING_PACKAGES=true
- WEBHOOK_URL=https://n8n.chensoul.cc/
- N8N_HOST=n8n.chensoul.cc
- N8N_ENCRYPTION_KEY=2cc5b5f9e31b43ff3817c1d04e5fa735
ports:
- '5678:5678'
networks:
- vps
networks:
vps:
external: true
  1. 启动服务:
Terminal window
docker compose -f n8n.yaml up -d
docker logs -f n8n
  1. 配置 nginx 配置文件 /etc/nginx/conf.d/n8n.conf
server {
listen 80;
listen [::]:80;
server_name n8n.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name n8n.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:5678;
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_http_version 1.1;
proxy_set_header Connection 'upgrade';
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Origin 'https://n8n.chensoul.cc';
access_log /var/log/nginx/n8n.log combined buffer=128k flush=5s;
}
}

故障排查:如果出现 Origin header does NOT match the expected origin. (Origin: "n8n.chensoul.cc", Expected: "127.0.0.1:5678") 错误,需要在 nginx 配置里添加 proxy_set_header Origin 'https://n8n.chensoul.cc';

  1. 重新加载 Nginx 配置并访问服务:
Terminal window
nginx -t
nginx -s reload
  1. 创建工作流(Workflows):

参考这篇文章 http://stiles.cc/archives/237/ ,目前我配置了以下 workflows,实现了 github、douban、rss、memos 同步到 Telegram。

workflows 参考:

Linkding

一款自托管的书签管理器,设计简洁、快速且易于设置。

创建数据库

Terminal window
docker exec postgres psql -U postgres -c "CREATE DATABASE linkding OWNER postgres;"

创建 docker-compose 文件 linkding.yaml

services:
linkding:
#image: woohoodai/linkding-cn:latest
image: sissbruecker/linkding
container_name: linkding
restart: unless-stopped
environment:
- LD_SUPERUSER_NAME=admin
- LD_SUPERUSER_PASSWORD=E2KWxxxx
- LD_DB_ENGINE=postgres
- LD_DB_DATABASE=linkding
- LD_DB_HOST=postgres
- LD_DB_USER=postgres
- LD_DB_PASSWORD=vps@027!
ports:
- "9090:9090"
networks:
- vps
networks:
vps:
external: true

启动服务:

Terminal window
docker compose -f linkding.yaml up -d
docker logs -f linkding

配置 nginx 配置文件 /etc/nginx/conf.d/linkding.conf

server {
listen 80;
listen [::]:80;
server_name linkding.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name linkding.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:9090;
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_http_version 1.1;
proxy_set_header Connection 'upgrade';
proxy_set_header Upgrade $http_upgrade;
access_log /var/log/nginx/linkding.log combined buffer=128k flush=5s;
}
}

重新加载 Nginx 配置并访问服务:

Terminal window
nginx -t
nginx -s reload

登录之后进行设置 Auto Tagging:

Terminal window
youtube.com youtube video
github.com github
bilibili.com bilibili video

完整的备份:

Terminal window
docker exec -it linkding python manage.py full_backup /etc/linkding/data/backup.zip
docker cp linkding:/etc/linkding/data/backup.zip backup.zip

使用:

  • 查询上周书签列表,返回 markdown 地址
Terminal window
start=$(date -v-1w -v-mon +%Y-%m-%dT00:00:00Z)
end=$(date -v-mon +%Y-%m-%dT00:00:00Z)
curl -sS -H "Authorization: Token ${LINKDING_TOKEN}" \
"https://linkding.chensoul.cc/api/bookmarks/?limit=100" \
| jq -r --arg start "$start" --arg end "$end" '
.results[]?
| select(.date_added >= $start and .date_added < $end)
| "- [" + (.title // "无标题") + "](" + .url + ")"
'

Artalk

在 postgres 容器创建 artalk 数据库:

Terminal window
docker exec postgres psql -U postgres -c "CREATE DATABASE artalk OWNER postgres;"

通过 docker compose 安装,创建 artalk.yaml

services:
artalk:
container_name: artalk
image: artalk/artalk-go
restart: unless-stopped
ports:
- "23366:23366"
environment:
- TZ=Asia/Shanghai
- ATK_LOCALE=zh-CN
- ATK_SITE_DEFAULT=Chensoul Blog
- ATK_SITE_URL=https://blog.chensoul.cc
- ATK_TRUSTED_DOMAINS=https://blog.chensoul.cc http://localhost:1313
- ATK_DB_TYPE=pgsql
- ATK_DB_HOST=postgres
- ATK_DB_PORT=5432
- ATK_DB_NAME=artalk
- ATK_DB_USER=postgres
- ATK_DB_PASSWORD=vps@027!
- ATK_ADMIN_NOTIFY_BARK_ENABLED=true
- ATK_ADMIN_NOTIFY_BARK_SERVER=https://bark.chensoul.cc/AXWM5ZKnKa9Lp3rijBsVDm
- ATK_ADMIN_NOTIFY_TELEGRAM_ENABLED=true
- ATK_ADMIN_NOTIFY_TELEGRAM_API_TOKEN=8286091453:AAE2pucQXfliz_QGac1zVZEFIrrxVOLe938
- ATK_ADMIN_NOTIFY_TELEGRAM_RECEIVERS=[-1001632154815]
networks:
- vps
networks:
vps:
external: true

启动服务:

Terminal window
docker compose -f artalk.yaml up -d
docker logs -f artalk

配置 nginx 配置文件 /etc/nginx/conf.d/artalk.conf

server {
listen 80;
listen [::]:80;
server_name artalk.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name artalk.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:23366;
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;
}
}

重新加载 Nginx 配置:

Terminal window
nginx -t
nginx -s reload

执行命令创建管理员账户:

Terminal window
docker exec -it artalk artalk admin

浏览器输入 http://artalk.chensoul.cc 进入 Artalk 后台登录界面。

Wakapi

在 postgres 容器创建 wakapi 数据库:

Terminal window
docker exec postgres psql -U postgres -c "CREATE DATABASE wakapi OWNER postgres;"

通过 docker compose 安装,创建 wakapi.yaml

services:
wakapi:
image: ghcr.io/muety/wakapi
container_name: wakapi
restart: always
ports:
- "3020:3000"
environment:
- WAKAPI_PASSWORD_SALT=chensoul
- WAKAPI_DB_TYPE=postgres
- WAKAPI_DB_NAME=wakapi
- WAKAPI_DB_PORT=5432
- WAKAPI_DB_HOST=postgres
- WAKAPI_DB_USER=postgres
- WAKAPI_DB_PASSWORD=vps@027!
volumes:
- wakapi_data:/data
networks:
- vps
volumes:
wakapi_data:
networks:
vps:
external: true

启动服务:

Terminal window
docker compose -f wakapi.yaml up -d
docker logs -f wakapi

配置 nginx 配置文件 /etc/nginx/conf.d/wakapi.conf

server {
listen 80;
listen [::]:80;
server_name wakapi.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name wakapi.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:3020;
proxy_set_header Host $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_set_header REMOTE-HOST $remote_addr;
proxy_buffering off;
}
}

重新加载 Nginx 配置:

Terminal window
nginx -t
nginx -s reload

Uptime

Uptime Kuma 是一个易于使用的自托管监控工具,可以监控网站和服务的可用性。

项目地址:https://github.com/louislam/uptime-kuma

使用 docker compose 部署,创建 uptime.yaml

services:
uptime:
image: louislam/uptime-kuma:2
container_name: uptime
volumes:
- ~/.uptime-kuma://app/data
ports:
- "3001:3001"
restart: always
networks:
- vps
networks:
vps:
external: true

启动服务:

Terminal window
docker compose -f uptime.yaml up -d
docker logs -f uptime

配置 nginx 配置文件 /etc/nginx/conf.d/uptime.conf

server {
listen 80;
listen [::]:80;
server_name uptime.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name uptime.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:3001;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

重新加载 Nginx 配置并访问服务:

Terminal window
nginx -t
nginx -s reload

访问 https://uptime.chensoul.cc/ ,首次访问需要设置管理员账号。

升级服务

Terminal window
docker compose -f uptime.yaml down
docker pull louislam/uptime-kuma:2
docker compose -f uptime.yaml up -d

Bark

Bark 是一个 iOS 应用,允许你向 iPhone 推送自定义通知。通过自建服务器,可以实现消息推送的完全控制。

项目地址:https://github.com/Finb/Bark

  1. 服务端使用 Docker 安装:
Terminal window
docker run -dt --name bark -p 127.0.0.1:3020:8080 -v ~/.bark/data://data finab/bark-server
  1. 配置 nginx 配置文件 /etc/nginx/conf.d/bark.conf
server {
listen 80;
listen [::]:80;
server_name bark.chensoul.cc;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name bark.chensoul.cc;
ssl_certificate /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/chensoul.cc.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:3020;
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;
}
}
  1. 重新加载 Nginx 配置:
Terminal window
nginx -t
nginx -s reload
  1. 在 iOS 上安装 Bark 应用,添加私有服务器:`http://bark.chensoul.cc

  2. 测试推送功能:

Terminal window
# 替换 <device_key> 为你的设备密钥
curl -X "POST" "https://bark.chensoul.cc/<device_key>" \
-H 'Content-Type: application/json; charset=utf-8' \
-d '{
"body": "这是一个测试",
"title": "Test Title",
"badge": 1,
"category": "myNotificationCategory",
"icon": "https://www.usememos.com/logo-rounded.png",
"group": "test"
}'

Docker 服务运维

容器运维

通过 docker 重启容器:

Terminal window
docker restart $(docker ps -q)
#或者重启所有容器(包括已停止的):
docker restart $(docker ps -aq)

通过 docker compose 文件重启:

Terminal window
docker compose -f rsshub.yaml down
docker compose -f n8n.yaml down
docker compose -f linkding.yaml down
docker compose -f umami.yaml down
docker compose -f uptime.yaml down
docker compose -f artalk.yaml down
docker compose -f memos.yaml down
docker compose -f rsshub.yaml up -d
docker compose -f n8n.yaml up -d
docker compose -f linkding.yaml up -d
docker compose -f umami.yaml up -d
docker compose -f uptime.yaml up -d
docker compose -f artalk.yaml up -d
docker compose -f memos.yaml up -d

查看内容使用情况:

Terminal window
$ docker stats --no-stream --format "table {{.Name}}\t{{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
NAME CONTAINER CPU % MEM USAGE / LIMIT MEM %
rsshub 97ca49881678 0.00% 213.4MiB / 512MiB 41.68%
redis 39164aeb6228 0.31% 35.52MiB / 128MiB 27.75%
postgres f46745208c1d 0.00% 45.6MiB / 1GiB 4.45%
n8n 338fbdac295d 0.37% 197.5MiB / 512MiB 38.57%
linkding a0e10cc27e18 0.14% 109.1MiB / 256MiB 42.60%
umami 80261017d1af 0.02% 9.277MiB / 256MiB 3.62%
uptime-kuma a4fa6a45a8d6 0.00% 32.17MiB / 256MiB 12.57%
artalk f9a10453002b 0.07% 11.23MiB / 256MiB 4.39%
memos 363b0964fad8 0.00% 13.02MiB / 4.055GiB 0.31%

数据库备份

先下载 rclone

Terminal window
curl -OfsS https://downloads.rclone.org/rclone-current-linux-amd64.zip
unzip -oq rclone-current-linux-amd64.zip
sudo mv rclone-*-linux-amd64/rclone /usr/local/bin/
sudo chmod +x /usr/local/bin/rclone

设置配置文件:

Terminal window
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf << 'EOF'
[r2]
type = s3
provider = Cloudflare
access_key_id = 4201cbab7617760d89d1837a93415f1d
secret_access_key = xxxxxxx
endpoint = https://2e1980a0ef43f57b920ea28cdf9d38d1.r2.cloudflarestorage.com
acl = private
no_check_bucket = true
EOF

备份脚本:/opt/vps-backup/scripts/backup_database.sh

#!/bin/bash
set -euo pipefail
CONTAINER_NAME="postgres"
BACKUP_BASE="/data/backup"
BACKUP_DIR="$BACKUP_BASE/postgres"
DATE=$(date +%Y%m%d_%H00) # 如 20260303_1601
DATABASES=(memos umami linkding artalk wakapi n8n)
mkdir -p "$BACKUP_DIR"
SQL_FILES=()
for DB_NAME in "${DATABASES[@]}"; do
BACKUP_FILE="${DB_NAME}_${DATE}.sql"
docker exec "$CONTAINER_NAME" pg_dump -U postgres "$DB_NAME" > "$BACKUP_DIR/$BACKUP_FILE"
SQL_FILES+=("$BACKUP_FILE")
echo "DB $DB_NAME backup success: $BACKUP_FILE"
done
# 将本次所有 .sql 打成一个 tar.gz(仍用 gzip 压缩)
ARCHIVE="${CONTAINER_NAME}_${DATE}.tar.gz"
tar -czf "$BACKUP_DIR/$ARCHIVE" -C "$BACKUP_DIR" "${SQL_FILES[@]}"
echo "Archive: $ARCHIVE"
# 已打包后可删本次 .sql,避免占双倍空间;若要在 git 里保留明文 sql 请注释下面两行
for f in "${SQL_FILES[@]}"; do
rm -f "$BACKUP_DIR/$f"
done
# 删除超过 1 天的 tar.gz(按修改时间)
find "$BACKUP_DIR" -maxdepth 1 -type f -name '${CONTAINER_NAME}_*.tar.gz' -mtime +1 -delete
rclone sync $BACKUP_DIR r2:backup/postgres -P

N8n 备份脚本:/opt/vps-backup/scripts/backup_n8n.sh

#!/bin/bash
set -euo pipefail
BACKUP_BASE="/data/backup"
BACKUP_DIR="$BACKUP_BASE/n8n"
WORKFLOWS_DIR="$BACKUP_DIR/workflows"
DATE=$(date +%Y%m%d_%H00) # 如 20260303_1600
mkdir -p $BACKUP_DIR/workflows
# 导出工作流到容器临时目录,再拷出并按名称重命名
WORKFLOWS_TMP=$(mktemp -d)
trap 'rm -rf "$WORKFLOWS_TMP"' EXIT
docker exec -u node n8n n8n export:workflow --backup --output=./backup_workflows
docker cp n8n:/home/node/backup_workflows/. "$WORKFLOWS_TMP"
for f in "$WORKFLOWS_TMP"/*; do
[ -f "$f" ] || continue
name=$(jq -r '.name' "$f" 2>/dev/null)
echo "$f $name"
[ -n "$name" ] && [ "$name" != "null" ] && command mv -f "$f" "$BACKUP_DIR/workflows/${name}.json"
done
# 导出凭证到当日目录
docker exec -u node n8n n8n export:credentials --all --output=./credentials.json
docker cp n8n:/home/node/credentials.json "$BACKUP_DIR/credentials.json"
rclone sync $BACKUP_DIR r2:backup/n8n -P

清理表数据

在确认无需保留数据的前提下,在目标库中执行:

  • 清空单张表(保留表结构,重置自增等):
    Terminal window
    docker exec "$CONTAINER_NAME" psql -U postgres -d "$DB_NAME" -c "TRUNCATE TABLE 表名;"
  • 清空单表并连同依赖该表外键的表一起清空
    Terminal window
    docker exec "$CONTAINER_NAME" psql -U postgres -d "$DB_NAME" -c "TRUNCATE TABLE 表名 CASCADE;"
  • 清空当前库下 public schema 中所有表(慎用):
    Terminal window
    docker exec "$CONTAINER_NAME" psql -U postgres -d "$DB_NAME" -c "
    DO \$\$
    DECLARE r RECORD;
    BEGIN
    FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public')
    LOOP
    EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE;';
    END LOOP;
    END \$\$;
    "

执行前建议先备份或确认库名、表名无误。

在容器内用 psql 查指定库中每个表的行数(以下为估算值,来自 pg_stat_user_tables,大表足够参考):

Terminal window
CONTAINER_NAME="postgres"
DB_NAME="n8n" # 改成要查的库名
docker exec "$CONTAINER_NAME" psql -U postgres -d "$DB_NAME" -c "
SELECT schemaname AS schema,
relname AS table_name,
n_live_tup AS row_estimate
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;
"

若要精确行数(大表较慢),可先生成再执行:

Terminal window
docker exec "$CONTAINER_NAME" psql -U postgres -d "$DB_NAME" -t -A -c "
SELECT 'SELECT count(*) AS \"' || relname || '\" FROM ' || quote_ident(schemaname) || '.' || quote_ident(relname) || ';'
FROM pg_stat_user_tables;
"

把输出在 psql -d "$DB_NAME" 里执行即可。

数据库恢复

按备份时间戳恢复(备份目录 postgres/,文件命名:<库名>_<YYYYMMDDHH>.sql)。将 BACKUP_POSTFIX 改为要恢复的那次备份时间(如 2026030316),在备份仓库根或指定目录下执行。

先停止服务:

Terminal window
docker compose -f n8n.yaml down
docker compose -f linkding.yaml down
docker compose -f umami.yaml down
docker compose -f artalk.yaml down
docker compose -f memos.yaml down

再恢复数据库:

#!/usr/bin/env bash
set -euo pipefail
CONTAINER_NAME="postgres"
BACKUP_DIR="/opt/vps-backup/postgres"
BACKUP_POSTFIX="2026031809" # 改为实际备份时间
DATABASES=(memos umami linkding artalk)
# 判断数据库是否存在(存在返回 1,不存在返回 0)
db_exists() {
local db="$1"
docker exec "$CONTAINER_NAME" psql -U postgres -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = '$db';" 2>/dev/null | grep -q 1
}
for DB_NAME in "${DATABASES[@]}"; do
F="${DB_NAME}_${BACKUP_POSTFIX}.sql"
if [ ! -f "$BACKUP_DIR/$F" ]; then
echo "跳过(文件不存在): $F"
continue
fi
docker cp "$BACKUP_DIR/$F" "$CONTAINER_NAME:/tmp/$F"
if db_exists "$DB_NAME"; then
docker compose -f /opt/vps-backup/dockerfile/$DB_NAME.yaml down
# 数据库已存在:禁止新连接、断开已有连接、删库、重建
docker exec "$CONTAINER_NAME" psql -U postgres -d postgres -c "ALTER DATABASE $DB_NAME CONNECT LIMIT 0;" 2>/dev/null || true
docker exec "$CONTAINER_NAME" psql -U postgres -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid();" 2>/dev/null || true
sleep 1
docker exec "$CONTAINER_NAME" psql -U postgres -d postgres -c "DROP DATABASE IF EXISTS $DB_NAME;"
docker exec "$CONTAINER_NAME" psql -U postgres -d postgres -c "CREATE DATABASE $DB_NAME OWNER postgres;"
else
# 数据库不存在:直接新建
docker exec "$CONTAINER_NAME" psql -U postgres -d postgres -c "CREATE DATABASE $DB_NAME OWNER postgres;"
fi
docker exec "$CONTAINER_NAME" psql -U postgres -d "$DB_NAME" -f "/tmp/$F"
docker exec "$CONTAINER_NAME" rm -f "/tmp/$F"
echo "已恢复: $DB_NAME"
done

注意:n8n 数据量太大,新建数据库,然后通过重新导入 workflow 文件来恢复。

Terminal window
docker exec postgres psql -U postgres -c "CREATE DATABASE n8n OWNER postgres;"

然后启动 n8n 服务:

Terminal window
docker compose -f n8n.yaml up -d

恢复n8n工作流

Terminal window
BACKUP_DIR="/opt/vps-backup/n8n"
docker cp "$BACKUP_DIR/workflows" n8n:/home/node/restore_workflows
docker exec -u node n8n n8n import:workflow --separate --input=/home/node/restore_workflows
docker exec -u node n8n rm -rf /home/node/restore_workflows

恢复n8n凭证

Terminal window
docker cp "$BACKUP_DIR/$DATE/credentials.json" n8n:/home/node/
docker exec -u node n8n n8n import:credentials --input=/home/node/credentials.json
docker exec -u node n8n rm -f /home/node/credentials.json

说明:

若当前库中已有同 ID 的工作流或凭证,导入时会覆盖。恢复后若 n8n 已在运行,工作流/凭证变更会生效,必要时可重启容器。

**发布n8n工作流:**登录网站,依次点击发布按钮。

定时任务

使用 crontab 设置定时任务,实现自动化运维。可手动 crontab -e 添加,或用下面命令一次性写入(不重复添加):

Terminal window
# 写入 crontab(若已有同类任务会重复,建议先 crontab -l 查看后选用其一方式)
( crontab -l 2>/dev/null | grep -v 'acme.sh --cron' | grep -v 'backup_database.sh' | grep -v 'backup_n8n.sh'; \
echo '58 1 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null'; \
echo '0 */3 * * * sh /opt/vps-backup/scripts/backup_database.sh'; \
echo '0 2 * * * sh /opt/vps-backup/scripts/backup_n8n.sh'; \
) | crontab -
# 查看当前任务
crontab -l

说明:SSL 证书续期(每天 1:58)、数据库备份(每 3 小时)、N8n 工作流备份(每天 2:00)。时钟同步由 chronyd 常驻,无需 crontab。

附录

AlmaLinux 9 一键初始化脚本

将下文脚本保存为 almalinux9-setup.sh,按需修改顶部变量或通过环境变量传入,以 root 执行 bash almalinux9-setup.sh。涵盖系统设置、安全加固、Nginx、SSL(acme.sh)、Docker;可选步骤均由变量控制。安全加固(SSH_KEY_ONLY 等)请确认密钥或普通用户可用后再开启;SSL 申请需 CloudFlare 凭证与 DOMAIN,未配置则只安装 acme.sh 与证书目录。

#!/usr/bin/env bash
# AlmaLinux 9 一键初始化:系统设置 + Nginx + SSL(acme.sh)+ Docker
# 用法:修改变量或环境变量后执行 bash almalinux9-setup.sh(需 root)
set -e
# ---------- 系统设置 ----------
HOSTNAME="${HOSTNAME:-vps}"
MIRROR="${MIRROR:-}"
GIT_USER="${GIT_USER:-chensoul}"
GIT_EMAIL="${GIT_EMAIL:-ichensoul@gmail.com}"
CREATE_USER="${CREATE_USER:-}"
SWAP_GB="${SWAP_GB:-0}"
SSH_KEY_ONLY="${SSH_KEY_ONLY:-no}"
PERMIT_ROOT="${PERMIT_ROOT:-prohibit-password}"
INSTALL_FAIL2BAN="${INSTALL_FAIL2BAN:-no}"
# ---------- Nginx / SSL / Docker ----------
INSTALL_NGINX="${INSTALL_NGINX:-yes}"
INSTALL_SSL="${INSTALL_SSL:-yes}"
INSTALL_DOCKER="${INSTALL_DOCKER:-yes}"
DOMAIN="${DOMAIN:-}"
CF_Token="${CF_Token:-}"
CF_Account_ID="${CF_Account_ID:-}"
CF_Key="${CF_Key:-}"
CF_Email="${CF_Email:-$GIT_EMAIL}" # 不设则与 GIT_EMAIL 一致
[[ $(id -u) -eq 0 ]] || { echo "请使用 root 运行"; exit 1; }
echo "[1/11] 设置主机名 ..."
[[ -z "$HOSTNAME" ]] || hostnamectl set-hostname "$HOSTNAME"
echo "[2/11] 配置 dnf 源(清理默认并创建精简 repo) ..."
case "$MIRROR" in
"") REPO_BASE="https://repo.almalinux.org" ;;
aliyun) REPO_BASE="https://mirrors.aliyun.com" ;;
aws-bos) REPO_BASE="https://bos.aws.repo.almalinux.org" ;;
aws-den) REPO_BASE="https://den.aws.repo.almalinux.org" ;;
*) echo "未知 MIRROR: $MIRROR"; exit 1 ;;
esac
mkdir -p /etc/yum.repos.d/bak
mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/bak/ 2>/dev/null || true
cat > /etc/yum.repos.d/almalinux.repo << REPOEOF
[baseos]
name=AlmaLinux 9 - BaseOS
baseurl=$REPO_BASE/almalinux/9/BaseOS/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9
[appstream]
name=AlmaLinux 9 - AppStream
baseurl=$REPO_BASE/almalinux/9/AppStream/\$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9
REPOEOF
dnf clean all && dnf makecache
echo "[3/11] 更新系统并安装常用软件 ..."
dnf update -y
dnf install -y git vim jq chrony
echo "[4/11] 时钟同步与时区 ..."
systemctl enable chronyd && systemctl start chronyd
chronyc -a makestep || true
timedatectl set-timezone Asia/Shanghai
echo "[5/11] Git 全局配置 ..."
git config --global user.name "$GIT_USER"
git config --global user.email "$GIT_EMAIL"
echo "[6/11] 可选:创建 sudo 用户 ..."
if [[ -n "$CREATE_USER" ]]; then
if ! id "$CREATE_USER" &>/dev/null; then
useradd -m -s /bin/bash "$CREATE_USER"
usermod -aG wheel "$CREATE_USER"
echo "请为 $CREATE_USER 设置密码:"
passwd "$CREATE_USER"
else
usermod -aG wheel "$CREATE_USER"
echo "用户 $CREATE_USER 已存在,已加入 wheel。"
fi
fi
echo "[7/11] 可选:Swap ..."
if [[ "$SWAP_GB" -gt 0 ]] && [[ ! -f /swapfile ]]; then
fallocate -l "${SWAP_GB}G" /swapfile
chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile swap swap defaults 0 0' >> /etc/fstab
swapon --show
fi
echo "[8/11] 可选:SSH 加固与 fail2ban ..."
if [[ "$SSH_KEY_ONLY" == "yes" || "$PERMIT_ROOT" != "yes" ]]; then
cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
if [[ "$SSH_KEY_ONLY" == "yes" ]]; then
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/; s/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
grep -q '^PasswordAuthentication ' /etc/ssh/sshd_config || echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config
grep -q '^PubkeyAuthentication ' /etc/ssh/sshd_config || echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config
fi
if [[ "$PERMIT_ROOT" != "yes" ]]; then
sed -i "s/^#*PermitRootLogin.*/PermitRootLogin $PERMIT_ROOT/" /etc/ssh/sshd_config
grep -q '^PermitRootLogin ' /etc/ssh/sshd_config || echo "PermitRootLogin $PERMIT_ROOT" >> /etc/ssh/sshd_config
fi
systemctl restart sshd
fi
if [[ "$INSTALL_FAIL2BAN" == "yes" ]]; then
dnf install -y epel-release
dnf install -y fail2ban fail2ban-firewalld
systemctl enable fail2ban --now
fi
echo "[9/11] Nginx ..."
if [[ "$INSTALL_NGINX" == "yes" ]]; then
if [[ ! -f /etc/yum.repos.d/nginx.repo ]]; then
cat > /etc/yum.repos.d/nginx.repo << 'NGINXREPO'
[nginx-stable]
name=nginx stable repo
baseurl=https://nginx.org/packages/rhel/9/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
NGINXREPO
fi
dnf remove -y nginx-core nginx-filesystem nginx-mod-* 2>/dev/null || true
dnf install -y nginx
systemctl enable nginx && systemctl start nginx
dnf install -y firewalld 2>/dev/null || true
firewall-cmd --zone=public --permanent --add-service=http 2>/dev/null || true
firewall-cmd --zone=public --permanent --add-service=https 2>/dev/null || true
firewall-cmd --reload 2>/dev/null || true
setsebool -P httpd_can_network_connect on 2>/dev/null || true
echo "Nginx 安装完成。"
fi
echo "[10/11] SSL(acme.sh)..."
if [[ "$INSTALL_SSL" == "yes" ]]; then
mkdir -p /etc/nginx/ssl
if [[ ! -x /root/.acme.sh/acme.sh ]]; then
curl -sSL https://get.acme.sh | sh -s "email=$GIT_EMAIL"
fi
export CF_Token CF_Account_ID CF_Key CF_Email
if [[ -n "$DOMAIN" ]] && { [[ -n "$CF_Token" ]] || [[ -n "$CF_Key" ]]; }; then
echo "申请证书:$DOMAIN 与 *.$DOMAIN ..."
/root/.acme.sh/acme.sh --issue --server letsencrypt --dns dns_cf -d "$DOMAIN" -d "*.$DOMAIN" --ecc
cp /root/.acme.sh/"${DOMAIN}"_ecc/{"$DOMAIN".cer,"$DOMAIN".key,fullchain.cer,ca.cer} /etc/nginx/ssl/
/root/.acme.sh/acme.sh --installcert -d "$DOMAIN" -d "*.$DOMAIN" --ecc \
--cert-file /etc/nginx/ssl/"$DOMAIN".cer \
--key-file /etc/nginx/ssl/"$DOMAIN".key \
--fullchain-file /etc/nginx/ssl/fullchain.cer \
--ca-file /etc/nginx/ssl/ca.cer \
--reloadcmd "nginx -s reload"
echo "证书已安装并配置自动续期。"
else
echo "未设置 DOMAIN 或 CloudFlare 凭证,仅完成 acme.sh 与 /etc/nginx/ssl。"
fi
fi
echo "[11/11] Docker ..."
if [[ "$INSTALL_DOCKER" == "yes" ]]; then
if ! command -v docker &>/dev/null; then
curl -fsSL https://get.docker.com | sh
fi
systemctl enable docker && systemctl start docker
iptables -P FORWARD ACCEPT 2>/dev/null || true
docker network create vps 2>/dev/null || true
echo "Docker 安装完成,已创建网络 vps(若不存在)。"
fi
echo "全部完成。"

变量说明

变量说明示例
HOSTNAME主机名,空则不设置vps
MIRROR软件源:空=默认,aliyunaws-bosaws-denaliyun
GIT_USER / GIT_EMAILGit 全局配置;邮箱同时用于 acme.sh 与 CloudFlare(CF_Email 未设时)-
CREATE_USER创建并加入 wheel 的用户名,空则不创建chensoul
SWAP_GBSwap 大小(GB),0 不配置2
SSH_KEY_ONLYyes=仅密钥登录,请先确认密钥可用no
PERMIT_ROOTprohibit-password / no / yesprohibit-password
INSTALL_FAIL2BANyes 安装 fail2banno
INSTALL_NGINXyes 安装 Nginx + 防火墙 + SELinuxyes
INSTALL_SSLyes 安装 acme.sh;有 DOMAIN+CF 凭证则申请证书yes
INSTALL_DOCKERyes 安装 Docker + 网络 vpsyes
DOMAIN主域名(通配符 *.$DOMAIN),空则不申请证书chensoul.cc
CF_TokenCloudFlare API Token(推荐)-
CF_Account_IDCloudFlare Account ID,与 CF_Token 配合使用-
CF_Key或使用 Global API Key(邮箱同 GIT_EMAIL,亦可设 CF_Email 覆盖)-

示例

Terminal window
cd /opt
git clone git@github.com:chensoul/vps-backup.git
cd vps-backup/scripts
# 仅系统设置 + Nginx + Docker,不申请证书
INSTALL_SSL=no bash almalinux9-setup.sh
# 系统设置 + 全部(含证书,需 CloudFlare 凭证)
MIRROR=aws-bos CREATE_USER=chensoul DOMAIN=chensoul.cc CF_Token="tEk7EtRfMHTC7kA9b_WwbJG-R4torcVzYhhBim3D" CF_Account_ID="2e1980a0ef43f57b920ea28cdf9d38d1" bash almalinux9-setup.sh

Docker 使用官方 get.docker.com;如需国内镜像或交互选源,可手动执行 bash <(curl -sSL https://linuxmirrors.cn/docker.sh) 后再运行本脚本(跳过 Docker 安装:INSTALL_DOCKER=no

服务部署脚本

前置:已将各服务的 postgres.yamlumami.yamlmemos.yamln8n.yamllinkding.yamlartalk.yaml 及(可选)uptime.yaml 放在同一目录,且 Nginx 配置已准备好(如放在该目录下 nginx/)。在该目录下执行:

Terminal window
set -e
DIR="${DIR:-$(pwd)}" # compose 文件所在目录,默认当前目录
cd "$DIR"
docker network create vps 2>/dev/null || true
docker compose -f postgres.yaml up -d
# 等 Postgres 就绪后创建数据库(非交互)
sleep 5
for db in umami memos n8n linkding artalk; do
docker exec postgres psql -U postgres -c "CREATE DATABASE $db OWNER postgres;" 2>/dev/null || true
done
docker compose -f umami.yaml up -d
docker compose -f memos.yaml up -d
docker compose -f n8n.yaml up -d
docker compose -f linkding.yaml up -d
docker compose -f artalk.yaml up -d
# 可选:docker compose -f uptime.yaml up -d
[ -d nginx ] && cp nginx/*.conf /etc/nginx/conf.d/ && nginx -t && nginx -s reload

定时任务与证书续期、备份等仍按上文「定时任务」一节单独配置即可。

参考文章

订阅文章

订阅更新,不错过后续文章

直接通过 RSS 和 Telegram 订阅本站更新。

分享文章

如果这篇有帮助,可以顺手转发

直接分享给同事、朋友,或者发到你的社交平台。