这篇文章上次修改于 329 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

前段时间我购买了几年的 CloudCone 特价机炸了,导致服务中断快一个星期,再加上没有定期做备份,服务无法快速恢复。我开始寻找其他服务商的机器,兜兜转转最终选择了搬瓦工的,因为看它比较老牌,口碑还不错,且国内直连的线路良好,在此发出一个 我的邀请码,你可以通过这个链接购买服务器,为博主提供微薄返利以资助网站的持续运营。

另外小扯一句,你可能会注意到我博客的更新频率有所下降。但其实是因为我以此同时还在维护另外一个名为 保罗的小窝 的网站。个人对于博客的定义是稍微正式的内容,而这个小窝更多的是分享随笔和流水账,各位可以自行选择订阅。

此前的方案

此前一直都在用 OneInstack 脚本来部署环境,尝试用它重新部署,却发现 MariaDB 无法正常安装。国内国外访问同一个包的下载地址居然行为是不一样的(国外出现 404)。我选择构建安装,时间太长太复杂,且选择 Caddy 作为 Web 服务器后,居然虚拟主机都没有创建成功,我怀疑脚本是不是根本就没做好 Caddy 的适配...

OneInstack 此前也爆过供应链挂马的问题,据说是被国内公司收购了之后出现,虽然作者承诺修复,但我仍对其安全性存在质疑。再三抉择后,只能忍痛放弃 OneInstack,打算从零开始学习部署属于自己的 Ubuntu 服务器环境了!

你需要一定的 Linux 使用基础才能更好的阅读本文,实际安装过程可能有些许曲折,本文对部分流程做了次序优化,可能存在欠缺需要自行分析和解决(例如某些软件包需要自行安装),虽然有一部分在编写过程中在虚拟机上重新执行确认过,但还是以实际操作为准吧!

环境配套

选择自己纯手工配置环境最主要的原因就是 PHP,它除了一个 FPM 服务以外,还需要一个 Web 服务器才能将项目跑起来,它并不能像 NodeJS 那样自己就是一个 HTTP 服务。再加上数据库服务放在相对实际的环境下可以更好的编写脚本实现一键备份等功能。使用 K8S / Docker 部署应用会更方便,但受限于成本、服务器数量和硬件配置等因素,自己纯手工配置环境依旧是首选方案。

  • Caddy
  • MariaDB
  • Redis
  • PHP 8.3

    • phpMyAdmin
  • NodeJS

    • FNM(Fast Node Manager)
    • PM2

在使用 Caddy 之前我都是选择 Nginx 作为 Web 服务器,Nginx 技术成熟但配置起来没有 Caddy 那么容易,Caddy 作为后起之秀其评价也还不错,它还自带 SSL 证书签发功能,而且不会出现因 HTTPS 证书导致的 IP 泄漏问题。此前也使用过它 部署环境 有些许经验,这次直接使用它作为生产环境我认为也是没有问题的!

MariaDB 是开源版本的 MySQL,此前保罗一直在用,因此就没有考虑 MySQL。

PHP 版本此前都是 7x,最新版本都是 8x,如果你要使用 PHP 的包管理器 Composer 管理项目,那么直接用 PHP 8 是最省时的选择(如果要用 7x,你还需要旧版本的 Composer,太麻烦了,我一个新手根本不会弄,还不如升级自己的代码)

phpMyAdmin 是一款运行在 PHP 下的老牌数据库管理软件,我们首先安装它也能确认 PHP 是否能正常与数据库进行连接。

NodeJS 也是我现在使用 Nuxt (Vue) 和 Remix (React) 框架构建应用所必备的,这里我们使用 FNM 安装,并配置 PM2 用于持续化启动网站。

实用软件

在开始之前我也推荐安装一些实用软件,你可以根据自己的需求选择安装。

安装 htop 以实时查看服务器配置:

sudo apt install vim htop fastfetch

安装 net-tools 以查看网络相关信息

sudo apt install net-tools

安装 trash-cli,防止文件直接删除,无法被恢复:

sudo apt install trash-cli
vim ~/.bash_aliases

alias rm='trash-put'

安装 ufw 以控制服务器的入站出站流量,选择性打开服务端口,防止被外部 IP 扫描减少安全风险。如果你的云服务商支持在后台自定义设置防火墙规则(例如阿里云),那么可以选择不安装。

sudo apt update
sudo apt install ufw

# 设置默认策略为拒绝所有传入连接
sudo ufw default deny incoming

# 设置默认允许所有传出连接
sudo ufw default allow outgoing

# 允许 TCP 端口 6755
sudo ufw allow 6755/tcp

# 检查状态
sudo ufw status verbose

# 启用 UFW
sudo ufw enable

安装 openssh-server 以允许远程操控服务器。如果是服务商提供的 Ubuntu,可能已经默认安装,直接跳过。

sudo apt install openssh-server

修改 SSHD 配置

编辑配置文件,修改端口,禁用密码访问,改为使用公私钥形式访问服务器。

sudo vim /etc/ssh/sshd_config

Port 5678
PubkeyAuthentication yes
PasswordAuthentication no

编辑文件 authorized_keys 添加私钥,以后将会用主机的公钥与服务器中的私钥配对连接。如果没有密钥对可以使用 ssh-keygen 生成一个,这里建议使用 ssh-keygen -t ed25519 指定算法生成(因为 Mac 那边新版本已经不支持默认的 RSA 了)

vim ~/.ssh/authorized_keys

重启服务器的 sshd 服务

sudo systemctl restart sshd

(可选)配置主机的 config 文件,指定使用证书命中到服务器。

Host MyServer
    Port 5678
    User root
    Hostname 10.7.9.103
    PreferredAuthentications publickey
    IdentityFile ~/.ssh/myserver_ed25519

安装 Caddy

参考 官网说明 安装即可。

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Caddy 已经完成安装并自动启动。如果你选择安装了 ufw 防火墙,允许外部访问 80 443 端口:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

新建站点目录

mkdir /var/www
mkdir /var/www/html

此时我们还不需要配置任何网站,你可以使用 wget 或在浏览器上访问服务器对应的 IP,确认 Caddy 已经正常启动。

安装 MariaDB

sudo apt install mariadb-server
sudo mysql_secure_installation

注意这里 Enter current password for root 第一次要求输入密码直接跳过,到后续第二次询问 Change the root password? 再设置。

至于它为什么会问两次密码,我也不是很清楚。我尝试过只在第一次要求的时候输入,但在第二次询问时跳过,这样做会导致最后什么密码都无法登录...

NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!

In order to log into MariaDB to secure it, we'll need the current
password for the root user. If you've just installed MariaDB, and
haven't set the root password yet, you should just press enter here.

Enter current password for root (enter for none): 
OK, successfully used password, moving on...

Setting the root password or using the unix_socket ensures that nobody
can log into the MariaDB root user without the proper authorisation.

You already have your root account protected, so you can safely answer 'n'.

Switch to unix_socket authentication [Y/n] n
 ... skipping.

You already have your root account protected, so you can safely answer 'n'.

Change the root password? [Y/n] n
 ... skipping.

By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them.  This is intended only for testing, and to make the installation
go a bit smoother.  You should remove them before moving into a
production environment.

Remove anonymous users? [Y/n] y
 ... Success!

Normally, root should only be allowed to connect from 'localhost'.  This
ensures that someone cannot guess at the root password from the network.

Disallow root login remotely? [Y/n] y
 ... Success!

By default, MariaDB comes with a database named 'test' that anyone can
access.  This is also intended only for testing, and should be removed
before moving into a production environment.

Remove test database and access to it? [Y/n] y
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

Reload privilege tables now? [Y/n] y
 ... Success!

Cleaning up...

All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!

安装 Redis

输入一条命令完成安装:

sudo apt install redis-server -y

安装 PHP

在安装之前,我们还需要新增一个 Ubuntu 软件源

sudo add-apt-repository ppa:ondrej/php
sudo apt update

安装 phpcomposer 及其对应的扩展(你可以根据自己的需要来安装):

sudo apt install php8.3 php8.3-curl php8.3-fpm php8.3-mysql php8.3-redis php8.3-mbstring php8.3-xml
sudo apt install composer
⚠️ 注意:使用 apt 安装部分 PHP 扩展时可能会导致安装 apache2 依赖项,后续需要将其禁用,可能使用 apt 来安装并不是一个最佳方案

编辑 php.ini 配置文件,取消生产环境隐藏报错的设置,好让我们接下来更好的排查问题,后期可视情况还原。

vim /etc/php/8.3/fpm/php.ini

error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING

display_errors = On

通过命令 service php8.3-fpm status 查看当前 php8.3-fpm 服务配置文件的路径。

include=/etc/php/8.3/fpm/pool.d/*.conf

可以看到引入了一个 pool 的配置文件,ls 列出当前文件夹下所有文件。

ls /etc/php/8.3/fpm/pool.d/
www.conf

使用 vim /etc/php/8.3/fpm/pool.d/www.conf 查看配置文件,即可看到 sock 的地址。

listen = /run/php/php8.3-fpm.sock

复制下来,后续我们需要将路径粘贴在 Caddy 的配置里面,后续粘贴到 Caddy 的字符串如下:

unix//run/php/php8.3-fpm.sock

新增第一个 Caddy 网站

首先创建所需的文件夹:

sudo mkdir /var/www/
sudo mkdir /var/www/html
sudo mkdir /var/www/html/default

还记得前面提到的 PHP Socks 路径嘛,这里将会用到,首先开始编辑 Caddyfile 配置文件:

vim /etc/caddy/Caddyfile

新增一条虚拟主机记录,由于访问地址是服务器 IP 自身,无法签 SSL 证书,因此直接选择自签即可。

10.7.9.103 {
        tls internal
        root * /var/www/html/default

        php_fastcgi unix//run/php/php8.3-fpm.sock
        file_server
}

重启服务,理论上没有任何问题。

service caddy restart

我们可以创建一个 index.php 文件,测试是否可以正常连接到 PHP:

vim /var/www/html/default/index.php
<?php

phpinfo();

使用浏览器访问链接 https://10.7.9.103,理论上将会显示当前 PHP 的运行信息。

安装 phpMyAdmin

官网 上下载最新版本,之后解压在 /var/www/html/default/phpMyAdmin 目录下。

官网上的版本已经内置了所需要的 Composer 依赖,我尝试过本地自行安装,但遇到了错误,原因不明,也不想花心思继续排查了,只要自己项目正常就行。

使用浏览器访问链接 https://10.7.9.103/phpMyAdmin,输入账号密码,你可能会遇到如下错误:

mysqli::real_connect(): (HY000/1698): Access denied for user 'root'@'localhost'`

此时主要检查两种情况,一个是前面密码的设置问题(可以用 mariadb 命令尝试登录),另外一个则可能是 root 账号所对应的 Host 的问题。

截至本文编写过程在虚拟机里重新执行确认时,并未出现这个错误,就挺奇怪的,但还是提供一下之前的解决方案

解决 root 账号访问问题

使用 mariadb -u root -p 命令登录,输入下面的命令,这里参考了 OneInstack 的源码。

# xxxx 是你的密码,可以和之前设置的一致
GRANT ALL PRIVILEGES ON *.* TO root@'localhost' IDENTIFIED BY "xxxx" WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON *.* TO root@'127.0.0.1' IDENTIFIED BY "xxxx" WITH GRANT OPTION;
FLUSH PRIVILEGES;

再次尝试,应该就能正常登录了。

安装 RClone 与设置备份

我注册了 CloudFlare R2 存储桶来完成服务器的备份数据存储,实测国内的银联信用卡也可以使用。可以在服务器上使用命令行工具 rclone 来将文件存储至 R2 存储桶,后期整理一个备份脚本定期执行即可。

https://developers.cloudflare.com/r2/examples/rclone/

Ensure you are running rclone v1.59 or greater (rclone downloads ↗). Versions prior to v1.59 may return HTTP 401: Unauthorized errors

CloudFlare 文档描述对 rclone 版本有要求,apt 下的似乎比较老,因此直接根据 官网教程 直接安装最新版本(截至本文编写过程,最新版本是 v1.68.0)

sudo -v ; curl https://rclone.org/install.sh | sudo bash

先在 CloudFlare 后台新建 R2 存储桶,并创建一个 API 令牌以供 rclone 程序进行连接。你将会得到以下几个数据:

  • 令牌值:env_auth
  • 访问密钥 ID:access_key_id
  • 机密访问密钥:secret_access_key
  • 为 S3 客户端使用管辖权地特定的终结点:endpoint

在编写自动化脚本之前,需要添加对应的存储桶配置,后期可直接使用对应的命令和配置上传文件。

rclone config

No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
n/s/q> n

Enter name for new remote.
name> cloudflare

Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.

4 (Amazon S3 Compliant Storage Providers including AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, DigitalOcean, Dreamhost, GCS, HuaweiOBS, IBMCOS, IDrive, IONOS, LyveCloud, Leviia, Liara, Linode, Magalu, Minio, Netease, Petabox, RackCorp, Rclone, Scaleway, SeaweedFS, StackPath, Storj, Synology, TencentCOS, Wasabi, Qiniu and others) -> 6 (Cloudflare R2 Storage)

Option provider.
Choose your S3 provider.
Choose a number from below, or type in your own value.
Press Enter to leave empty.

 6 / Cloudflare R2 Storage
   \ (Cloudflare)


Option env_auth.
Get AWS credentials from runtime (environment variables or EC2/ECS meta data if no env vars).
Only applies if access_key_id and secret_access_key is blank.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (false).

 1 / Enter AWS credentials in the next step.
   \ (false)
 2 / Get AWS credentials from the environment (env vars or IAM).
   \ (true)

回车继续,输入 access_key_idsecret_access_key

Option access_key_id.
AWS Access Key ID.
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
access_key_id>

Option secret_access_key.
AWS Secret Access Key (password).
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
secret_access_key>

Option region.
Region to connect to.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / R2 buckets are automatically distributed across Cloudflare's data centers for low latency.
   \ (auto)
region> 1

Option endpoint.
Endpoint for S3 API.
Required when using an S3 clone.
Enter a value. Press Enter to leave empty.
endpoint>

Edit advanced config?
y) Yes
n) No (default)
y/n> n

Configuration complete.
Options:
- type: s3
- provider: Cloudflare
- access_key_id:
- secret_access_key:
- region: auto
- endpoint:
Keep this "cloudflare" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> y

配置完成后,可使用命令列出存储桶的数据和上传文件,确保连接正常。

rclone tree cloudflare:存储桶名称

理论上能够正常列出存储桶下的内容,如果不能正常显示,可以从下面几个方向排查:

  • 存储桶名称不对
  • 代理设置问题
  • API Tokens 的权限设置问题(至少给到 Object Read & Write 权限)
  • API Tokens 的存储桶访问限制
  • API Tokens 的 IP 地址限制

打包生成备份的过程在这里不作详细介绍,大致就是使用 mysqldump 导出数据库,之后使用 tar 打包压缩站点数据,最后使用 rclone copy 命令将所有文件上传,定期手动或自动化执行都是可以的。

cd ~/backup/2024-10-21/
mysqldump -u root -p home > home.sql
rclone copy . cloudflare:存储桶名称/2024-10-21/

安装 NodeJS

官网 上选择对应的环境,即可生成一键安装脚本,我选择了当前最新的 LTS 版本和 fnm,你可根据自己的需要安装对应的版本。

# installs fnm (Fast Node Manager)
curl -fsSL https://fnm.vercel.app/install | bash

# activate fnm
source ~/.bashrc

# download and install Node.js
fnm use --install-if-missing 20

# verifies the right Node.js version is in the environment
node -v # should print `v20.17.0`

# verifies the right npm version is in the environment
npm -v # should print `10.8.2`

接着来安装 PM2,这是项目持久化运行在服务器上所必备的。

pm2 startup
pm2 start ./ecosystem.config.cjs(应用程序的示例,以实际为准)
pm2 save

这样服务器重启后,也能自动启动 pm2 服务以及对应的应用程序。

新建一个 NodeJS 虚拟主机

配置一个 NodeJS 项目到 Caddy 非常简单,基本上就只是一个端口转发规则而已。

paul.ren {
        reverse_proxy localhost:3001

        # 添加以下配置以处理 /upload 路径
        handle /upload/* {
                root * /var/www/html/legacy.paul.ren/public
                file_server
        }

        handle /static/* {
                root * /var/www/html/legacy.paul.ren/public
                file_server
        }
}

其他

可以编辑 ~/.bash_aliases 文件自定义自己的快捷命令,快速定位到 Caddyfile 配置等。

最后

感谢 @提莫 同学对本文中一些内容的指正,由于本人的运维经验不足,虽然已经花了不少时间重试其中的某些步骤,但仍然可能有所不足和缺漏,有什么问题可以在下方留言,我会尽力为大家解答。