简介:这是一套面向企业运维与建站开发者的私有化SAAS云建站系统源码,适合希望将建站服务部署在自有服务器、重视数据安全与定制化的团队。系统支持多用户独立账号与模板化建站,用户可登录后台完成内容编辑、发布与管理,并能在有限硬件资源下承载大量独立站点。压缩包共566个文件,约12.74MB,以224个Java后端源码、126个JSP页面、82个HTML模板及26个JS脚本为主体,另含CSS样式、PNG/JPG图片、XML与properties配置、SQL初始化脚本及少量jar依赖,覆盖前端界面、后端服务与数据库层。目前已有212人学习下载。借助这套源码,读者可研究SAAS建站系统的整体架构、多用户权限设计与模板机制,参考其数据库与缓存配置思路,并据此搭建私有化部署环境,为二次开发与性能调优提供可落地的代码基础。
1. 私有化部署自己的 SAAS 云建站系统:从“租户隔离”到“一键开站”的落地拆解
很多团队第一次动私有化部署 SAAS 云建站系统的念头,不是因为技术炫技,而是被现实逼的:客户要求数据必须留在自己机房,或者业务方希望把建站能力打包进自己的产品里卖给下游。这时候你面对的不是一个简单的 CMS,而是一套多租户、带独立域名绑定、支持模板切换和资源配额的系统。它的核心矛盾在于:既要让每个租户感觉自己独占一套建站工具,又要在同一套进程和数据库里把成本压到最低。适合谁做?有基本 Linux 和 Docker 经验的后端或运维,能看懂 Nginx 配置和 MySQL 表结构,愿意花两三天把租户隔离和域名解析这两件事啃下来。这篇文章不聊虚的,直接按“选型 → 部署 → 隔离 → 避坑 → 进阶”的顺序,把我在实际交付中踩过的坑和能抄的配置都摊开。
2. 私有化部署前的选型账:为什么多数人最后选了“单体多租户 + 独立库”
2.1 三种隔离级别的成本与风险对比
私有化部署 SAAS 云建站系统,第一个要拍板的就是租户隔离级别。常见做法有三种:共享库共享表(加 tenant_id 字段)、共享库独立表(按租户前缀分表)、独立库独立账号。我一般会先画一张表把账算清楚,再决定用哪种。
| 隔离级别 | 数据安全性 | 单机可承载租户数 | 迁移/备份难度 | 适合场景 |
|---|---|---|---|---|
| 共享表 + tenant_id | 低,一条 SQL 漏加条件就串数据 | 500+ | 低,整库导出 | 内部试用、预算极紧 |
| 共享库 + 独立表前缀 | 中,表名隔离但连接池共享 | 100~200 | 中,按前缀导出 | 中小型 SAAS 交付 |
| 独立库 + 独立账号 | 高,故障和备份完全隔离 | 30~80(单机) | 高,需逐库操作 | 政企私有化、等保要求 |
选独立库的理由很直接:私有化场景下客户最怕“我的数据被别人看见”,独立库能从物理层面切断这种可能。代价是连接数暴涨,MySQL 默认 151 个连接,50 个租户每个租户 3 个连接就吃满了。所以选型时一定要同步确认数据库最大连接数和应用侧连接池上限。
2.2 建站系统的技术栈怎么定:别被“全栈框架”带偏
云建站系统的本质是“模板渲染 + 资源管理 + 域名路由”,不是高并发交易系统。我见过有人上来就上微服务,结果私有化部署时客户只有一台 4C8G 的虚拟机,光服务发现就占了 2G 内存。常见且可靠的做法是:单体应用 + 多租户中间件 + 独立渲染进程。具体技术栈可以这样定:
- 应用层:Spring Boot 或 Laravel,选团队最熟的,别为了“先进”换语言。
- 租户识别:优先用域名,其次用请求头 X-Tenant-Id,两者都支持但以域名为主。
- 模板引擎:Thymeleaf / Blade / Twig 都行,关键是模板文件按租户目录存放。
- 资源存储:本地磁盘 + 租户目录隔离,私有化场景下对象存储往往不可用。
- 数据库:MySQL 8.0 或 PostgreSQL 14,独立库方案下每个租户一个 schema。
提示:私有化部署时客户经常没有外网,所有依赖必须提前打成离线包。Docker 镜像、Maven 仓库、npm 缓存,一个都不能少。
2.3 最小化部署清单:一台 4C8G 能跑多少租户
我实测过一台 4C8G 的 CentOS 7.9,Docker 24.0 + MySQL 8.0 + Nginx 1.24,跑独立库方案,每个租户一个 Spring Boot 实例(JVM 堆 256M),稳定承载 12~15 个租户。超过这个数,MySQL 连接数和 JVM 内存就开始互相挤。如果客户坚持要 50 个租户,要么升配到 8C16G,要么改成共享库独立表方案。这个数字不是拍脑袋,是压测时用 JMeter 模拟 50 并发建站请求,观察 CPU 和连接池活跃数得出的。部署前先跟客户对齐租户数量,再决定隔离级别,能省掉后面重构的麻烦。
3. 从零跑通最小闭环:Docker Compose 编排与租户初始化脚本
3.1 用 Docker Compose 拉起 MySQL、Nginx 和应用
私有化部署最怕环境不一致,Docker Compose 是平衡“简单”和“可复现”的最佳工具。下面这份 compose 文件是我在多个项目里改出来的最小可用版本,去掉了所有非必要组件,只保留 MySQL、Nginx 和建站应用。
version: "3.8" services: mysql: image: mysql:8.0 container_name: saas-mysql environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PWD} volumes: - ./data/mysql:/var/lib/mysql - ./init-sql:/docker-entrypoint-initdb.d command: --max_connections=500 --character-set-server=utf8mb4 ports: - "127.0.0.1:3306:3306" restart: always app: image: saas-builder:latest container_name: saas-app depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/builder_master?useSSL=false SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: ${DB_ROOT_PWD} TENANT_MODE: independent volumes: - ./data/tenants:/app/tenants - ./logs:/app/logs ports: - "127.0.0.1:8080:8080" restart: always nginx: image: nginx:1.24 container_name: saas-nginx volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./data/tenants:/var/www/tenants ports: - "80:80" - "443:443" restart: always逻辑说明:MySQL 挂载init-sql目录,容器首次启动时会自动执行里面的建库脚本;应用通过环境变量TENANT_MODE控制隔离模式;Nginx 把租户静态资源目录挂进来,后面做域名路由时直接指向对应租户目录。参数方面,max_connections=500是给独立库方案留余量,如果租户少可以降到 200。127.0.0.1:3306绑定本地回环,避免数据库端口暴露到公网,这是私有化部署的基本安全习惯。
3.2 租户初始化脚本:建库、建账号、写路由
应用启动后第一件事是初始化租户。我一般写一个 Python 脚本,读租户清单 CSV,批量建库、建账号、生成 Nginx 配置片段。下面这个脚本处理 20 个租户大约 8 秒。
import pymysql import csv import os MASTER_CONN = pymysql.connect( host="127.0.0.1", user="root", password=os.getenv("DB_ROOT_PWD") ) def init_tenant(tenant_id, domain): db_name = f"tenant_{tenant_id}" user = f"u_{tenant_id}" pwd = os.urandom(12).hex() # 每个租户独立随机密码 with MASTER_CONN.cursor() as cur: cur.execute(f"CREATE DATABASE IF NOT EXISTS {db_name} DEFAULT CHARSET utf8mb4;") cur.execute(f"CREATE USER IF NOT EXISTS '{user}'@'%' IDENTIFIED BY '{pwd}';") cur.execute(f"GRANT ALL PRIVILEGES ON {db_name}.* TO '{user}'@'%';") cur.execute(f"INSERT INTO builder_master.tenant_route (tenant_id, domain, db_name, db_user, db_pwd) VALUES ('{tenant_id}', '{domain}', '{db_name}', '{user}', '{pwd}');") MASTER_CONN.commit() # 生成 Nginx 配置片段 nginx_conf = f""" server {{ listen 80; server_name {domain}; root /var/www/tenants/{tenant_id}/public; location / {{ proxy_pass http://saas-app:8080; proxy_set_header Host $host; proxy_set_header X-Tenant-Id {tenant_id}; }} }} """ with open(f"./nginx/conf.d/{tenant_id}.conf", "w") as f: f.write(nginx_conf) with open("tenants.csv") as f: for row in csv.DictReader(f): init_tenant(row["tenant_id"], row["domain"])逻辑说明:脚本先连 master 库,为每个租户创建独立数据库和独立账号,密码用os.urandom生成,避免弱口令。然后把租户和域名的映射写入tenant_route表,应用侧靠这张表做域名到租户的解析。最后生成 Nginx 配置片段,X-Tenant-Id请求头是兜底识别手段。参数上,tenants.csv至少包含tenant_id和domain两列,tenant_id建议用短横线分隔的小写字母数字,避免数据库名和用户名出现特殊字符。执行完脚本后docker exec saas-nginx nginx -s reload让配置生效。
3.3 验证闭环:用 curl 模拟租户访问
部署完别急着交付,先用 curl 验证租户识别和静态资源隔离。假设租户 A 的域名是a.example.com,在客户内网 DNS 里已经解析到这台机器。
# 验证租户 A 的首页返回 200 且内容包含租户标识 curl -s -H "Host: a.example.com" http://127.0.0.1/ | grep -o "tenant-a" # 验证租户 B 无法访问租户 A 的静态资源 curl -s -o /dev/null -w "%{http_code}" -H "Host: b.example.com" http://127.0.0.1/tenants/a/logo.png # 期望返回 403 或 404,而不是 200逻辑说明:第一条命令用Host头模拟域名访问,检查返回内容里是否有租户 A 的标识,确认应用侧租户识别生效。第二条命令故意用租户 B 的域名去访问租户 A 的资源路径,如果返回 200 说明静态资源隔离没做好,需要检查 Nginx 的root是否按租户目录严格区分。参数上,-H "Host: ..."是本地验证多域名最方便的方式,不用改 hosts 文件。如果返回 502,先看应用容器是否正常监听 8080,再看 Nginx 的proxy_pass地址是否写错。
4. 租户隔离的深水区:域名路由、资源配额与数据备份
4.1 域名路由的三种实现方式与选择依据
私有化部署 SAAS 云建站系统,域名路由是租户感知的第一道门。常见做法有三种:Nginx 泛解析 + 应用侧查表、Nginx 按租户生成独立 server 块、应用层完全接管域名解析。我一般推荐第一种,原因是配置量小、新增租户不用 reload Nginx。
具体做法是:Nginx 配置一个泛解析 server,server_name ~^(?<subdomain>.+)\.example\.com$;,然后把$subdomain通过请求头传给应用,应用查tenant_route表找到对应租户。这样新增租户只需要在数据库插一条记录,Nginx 完全不用动。第二种方式适合租户数量少且域名固定的场景,每个租户一个 server 块,配置直观但维护成本高。第三种方式把域名解析完全交给应用,Nginx 只做四层转发,灵活但性能差,不推荐。
注意:泛解析依赖 DNS 的 wildcard 记录,私有化环境里客户的内网 DNS 不一定支持。部署前一定要确认 DNS 能不能配
*.example.com,否则只能走独立 server 块方案。
4.2 资源配额:别让一个租户的图片撑爆磁盘
多租户系统最怕“坏邻居”——一个租户上传了几十 G 的图片,把磁盘写满,所有租户一起挂。私有化场景下没有云厂商的自动扩容,必须提前做配额。我一般从三个维度限制:磁盘目录大小、单文件上传大小、租户总文件数。
磁盘目录大小用 Linux 的quota或xfs_quota实现,每个租户一个系统用户,挂载时启用usrquota。单文件上传大小在应用层限制,Spring Boot 里配spring.servlet.multipart.max-file-size=10MB。租户总文件数在数据库里记一个计数器,每次上传前检查。下面是一个简单的配额检查逻辑:
import os TENANT_QUOTA_MB = 2048 # 每个租户 2GB TENANT_MAX_FILES = 5000 def check_quota(tenant_id): tenant_dir = f"/app/tenants/{tenant_id}" total_size = sum( os.path.getsize(os.path.join(dp, f)) for dp, _, files in os.walk(tenant_dir) for f in files ) file_count = sum(len(files) for _, _, files in os.walk(tenant_dir)) if total_size > TENANT_QUOTA_MB * 1024 * 1024: raise Exception(f"租户 {tenant_id} 磁盘配额已满") if file_count > TENANT_MAX_FILES: raise Exception(f"租户 {tenant_id} 文件数超限")逻辑说明:os.walk遍历租户目录统计总大小和文件数,超过阈值就抛异常,应用层捕获后返回 403。参数上,TENANT_QUOTA_MB和TENANT_MAX_FILES建议做成配置文件,不同租户可以不同。这个检查放在上传接口的最前面,避免先写文件再检查导致磁盘被临时占满。如果租户数量多,每次遍历目录会慢,可以改成定时任务统计 + 缓存,上传时只读缓存。
4.3 数据备份:独立库方案下的逐库导出与恢复
独立库方案最大的好处是备份隔离,坏处是不能一条命令全备。我一般写一个备份脚本,遍历tenant_route表,逐库mysqldump,然后打包上传到客户指定的备份目录。恢复时按租户单独恢复,不影响其他人。
#!/bin/bash BACKUP_DIR="/backup/$(date +%Y%m%d)" mkdir -p $BACKUP_DIR mysql -h127.0.0.1 -uroot -p$DB_ROOT_PWD -N -e \ "SELECT tenant_id, db_name FROM builder_master.tenant_route" | \ while read tenant_id db_name; do mysqldump -h127.0.0.1 -uroot -p$DB_ROOT_PWD \ --single-transaction --routines --triggers \ $db_name > $BACKUP_DIR/${tenant_id}.sql done tar -czf $BACKUP_DIR.tar.gz $BACKUP_DIR逻辑说明:--single-transaction保证 InnoDB 表备份一致性,不锁表;--routines --triggers把存储过程和触发器一起导出,建站系统里模板相关的逻辑经常用触发器。参数上,BACKUP_DIR按日期分目录,方便保留多份。恢复时用mysql -uroot -p db_name < tenant_id.sql,注意先建库再导入。如果租户数据量大,可以加--compress减少备份文件体积,但会增加 CPU 开销。
5. 避坑与排查:私有化部署 SAAS 云建站系统最容易翻车的 5 个点
5.1 现象:新增租户后访问域名返回默认站点
原因:Nginx 泛解析配置里server_name没匹配上,请求落到了default_server。或者应用侧tenant_route表里没有这条记录,应用返回了默认租户页面。
解决:先curl -H "Host: 新域名"看返回内容是不是默认页,然后检查 Nginx 配置里泛解析正则是否覆盖新域名。再查tenant_route表有没有记录,没有就补插。最后确认 DNS 解析是否生效,私有化环境里 DNS 缓存时间可能很长,必要时让客户清缓存。
5.2 现象:租户上传图片后前台 404,但文件确实存在
原因:Nginx 的root指向了租户目录,但应用写入的路径和 Nginx 读取的路径不一致。常见的是应用写/app/tenants/a/public/upload/,Nginx 配的是/var/www/tenants/a/public/,两个目录没做映射。
解决:检查 Docker Compose 里应用的 volumes 和 Nginx 的 volumes 是否指向同一个宿主机目录。如果应用和 Nginx 在不同容器,要么挂同一个宿主机目录,要么用共享 volume。我一般统一挂./data/tenants:/app/tenants和./data/tenants:/var/www/tenants,保证两边看到同一份文件。
5.3 现象:MySQL 连接数暴涨,应用报 “Too many connections”
原因:独立库方案下每个租户一个连接池,租户数多了连接数线性增长。默认 HikariCP 每个池 10 个连接,50 个租户就是 500 个,直接打满 MySQL。
解决:把每个租户的连接池上限降到 3~5,并在 MySQL 侧把max_connections调到 500 以上。更彻底的做法是引入连接池代理,比如 ProxySQL,多个租户共享后端连接。但私有化场景下多一个组件多一份运维成本,我一般先调小连接池,不够再上代理。
5.4 现象:租户 A 的定时任务把 CPU 跑满,租户 B 访问变慢
原因:所有租户共享同一个应用进程,租户 A 的建站任务(比如批量生成静态页)没有限流,吃掉了全部 CPU。
解决:把重任务拆到独立线程池,按租户维度限流。Spring Boot 里可以给每个租户分配固定大小的ThreadPoolTaskExecutor,或者用信号量控制并发数。更简单的做法是给建站任务加队列,单线程消费,避免多个租户同时跑重任务。
5.5 现象:备份脚本执行到一半失败,部分租户数据丢失
原因:mysqldump逐库导出时,某个库太大导致超时,或者磁盘空间不足写不进去。脚本没有错误处理,继续往下跑,最后打包的备份不完整。
解决:在脚本里加set -e和每步的错误检查,mysqldump失败就退出并告警。备份前先检查磁盘剩余空间,至少留出数据库大小的 1.5 倍。另外,备份文件要校验,tar -tzf能列出内容才算完整。我习惯在备份完成后随机抽一个租户的 SQL 文件,head -20看有没有CREATE TABLE语句,确认不是空文件。
6. 进阶技巧:用“模板快照”把新租户开站时间压到 10 秒内
私有化部署 SAAS 云建站系统,客户最直观的体验就是“开站快不快”。如果每新增一个租户都要手动建库、导模板、配 Nginx,运维会疯。我后来用了一个“模板快照”的办法:预先建一个tenant_template库,里面放好所有基础表和默认模板数据,新增租户时直接CREATE DATABASE tenant_x TEMPLATE tenant_template(PostgreSQL 支持,MySQL 用mysqldump导入也行),然后只改租户配置表里的域名和账号信息。
具体步骤:先用mysqldump把模板库导出成template.sql,新增租户时执行mysql -uroot -p tenant_x < template.sql,再UPDATE tenant_config SET domain='新域名' WHERE id=1。整个过程在本地测试平均 8 秒,比从零初始化快 20 倍。Nginx 配置用泛解析,不用 reload。租户目录直接从模板目录cp -r一份,改一下权限。
验证方法:开完站后立刻用curl -H "Host: 新域名"请求首页,检查返回内容里的租户名称和域名是否匹配。再上传一张图片,确认能访问。最后查一下tenant_route表,确认记录写入成功。这套流程我用了两年多,唯一翻车的一次是模板库里带了测试数据,新租户开出来带了一堆假文章。后来在模板导出时加了--where="is_test=0"过滤,才彻底解决。
提示:模板快照方案适合租户结构完全一致的场景。如果不同租户需要不同的表结构,还是得走逐库初始化,别为了快牺牲灵活性。
我现在养成的习惯是:每次交付私有化部署,先跑一遍“开站 10 秒”的验收用例,再让客户自己开一个租户试试。能过这一关,后面基本不会有大问题。希望帮到你。
本文还有配套的精品资源,点击获取