这几年我研究最多的系统不是ERP也不是OA而是CRM。最开始用SaaS版数据在别人服务器上后来试过免费开源项目功能太死板再后来自己动手才有了这套DeskcommCRM。它算是一套自托管的CRM系统核心解决的问题很直白客户数据自己掌握、系统永久在线、字段流程按团队习惯改。如果你正好在带销售团队或者公司规模不大但客户信息越理越乱这篇分享会非常对口。1. 为什么我会自己搭一套CRM先说说那些踩过的坑1.1 SaaS版CRM的免费陷阱市面上的SaaS型CRM大多走免费试用路线用着用着你就会发现所谓的“免费”是有天花板的。用户数限制在十几个人客户字段个数被锁死自定义对象要开企业版导出Excel还给你加水印更别提开放API和Webhook这类高级能力基本都藏在最贵的套餐里。真正让我下决心放弃SaaS CRM的是一次续费涨价。当时团队只有二十多个人老客户信息导不出来换系统又怕丢数据只能硬着头皮接受新价格。那种“卡脖子”的感觉做过业务系统选型的人应该都懂。数据一旦上传到别人的系统迁移成本就成了软肋服务的定价权就在对方手里。所以我当时列了一份需求清单客户和跟进记录要能自定义字段销售流程能按我们自己的阶段走全员可同时在线数据绝对在自己数据库里还需要有按天自动备份。这套清单后来就成了DeskcommCRM的产品框架。如果你也在犹豫要不要自建CRM先算一笔长期账自建方案的前期投入是服务器成本和部署时间换来的是一年两年后不再被SaaS厂商的套餐框住数据边界清楚功能修改只需要改自己的代码或配置。1.2 免费CRM与私人网站的差别到底差在哪“免费CRM”和“私人网站”听起来完全不同但在实际部署场景里它们指向的是两种形态一种是注册即用的公有云多租户服务另一种是部署在自己服务器或云主机上的私有系统。差别主要体现在下面几项。对比项免费SaaS型CRM自建私人CRMDeskcommCRM形态数据归属存在厂商数据库中导出受限制存在自己数据库里随时导出、随时销毁可定制性受套餐限制字段和流程固定表和字段都能改流程按团队配置用户数成本超出免费额度后按人头收费增加用户不影响软件费用只轻微增加服务器负载系统可用性依赖厂商运维厂商停机全员不可用只要服务器运行就永久在线可自设备份和自启二次开发一般需要等厂商开放有代码能力可直接改没有也可用后台配置满足多数需求这个差异其实很本质免费SaaS拿你的使用数据作为产品迭代素材而自建系统把数据生命周期完整交还给你。永久在线这个词在自建场景下的意思就是——你的CRM是否在线只取决于你自己的服务器和网络而不是一家你完全不认识的公司某次升级带来的意外中断。所以如果你在搜索“免费CRM与私人网站的区别”这类问题大概率已经遇到了两个痛点一是免费的SaaS版功能不够用二是对数据归属不放心。DeskcommCRM这个项目就是第三条路不是免费但成本可控不是商业产品但比商业产品更听话。2. DeskcommCRM整体是怎么设计的2.1 核心模块与业务流程一个CRM系统拢共就干几件事客户管理、跟进记录、销售阶段、提醒与统计、团队成员协作。DeskcommCRM在设计时没有堆砌大而全的功能而是把下面的闭环跑通了线索导入 → 客户分配 → 跟进记录 → 销售阶段流转 → 成交归档。客户模块是我最重视的部分。因为不同团队对“客户”的理解完全不同做项目制销售的要记录甲方项目预算和负责人做零售的要记录购买偏好和复购周期做B2B服务商的需要关联多个联系人和合同。所以客户表没有采用固定字段而是做成了动态字段配置。管理员在后台自行添加字段类型比如单行文本、下拉选项、日期、金额、关联对象表单保存后前端列表和详情页会自动刷新。跟进记录是销售团队每天用最多的功能。它的设计要点不是记录本身而是“不可篡改”和“响应及时”。员工写一条跟进记录后系统会留下操作人和时间戳无论谁在哪天跟进过什么内容历史都对团队负责人可见。同时每次记录后可以手动设定下一次提醒时间到时候系统会在站内消息和邮件双通道通知对应销售。销售阶段在DeskcommCRM里是可视化看板的形态。每个客户的阶段状态会形成一张卡片拖拽就能移动比如从“初次联系”拖到“需求确认”。看板上的每个阶段都可以设定赢单率统计面板会自动算出销售预测金额。这个设计用了一个月后我们发现最直接的收益是每天早会不用再挨个听销售汇报打开看板就知道谁手里有多少单子卡在哪个阶段。2.2 技术选型为什么要这么搭技术栈的选择我考虑了很久最终定为后端用Node.js前端用Vue 3数据库用PostgreSQL缓存用Redis部署用Docker Compose反向代理用Caddy。Node.js在后端生态里有一个很大的优势迭代速度快和前端共用JavaScript语法团队里一个人能同时维护前后端逻辑。Express框架的中间件设计在实现登录鉴权、接口权限校验时特别灵活。我踩过不少框架的坑相比Spring全家桶的重配置和PHP的老旧生态Node.js做这种业务系统是投入产出比最高的。选PostgreSQL而不是MySQL核心原因有两个一是PostgreSQL对JSON字段的支持非常完整正好适配我更“大幅度定制客户字段”的需求——新增字段只更新一条配置记录不用频繁执行ALTER TABLE二是PostgreSQL的pg_dump备份机制在恢复时极其稳定这对“永久在线”和数据安全是很关键的。等到团队规模变大PostgreSQL的复杂查询能力和索引优化也完全接得住。Docker Compose是自托管服务的省心关键。整个DeskcommCRM由三个容器组成应用服务、PostgreSQL、Redis。所有依赖都用一条命令启动换服务器迁移也只需复制数据目录和编排文件。配合restart: always策略服务器意外重启之后系统会自动拉起全部服务这就是“永久在线”的底层保障。反向代理选择Caddy而非Nginx是因为Caddy能自动申请和续期HTTPS证书。早期我用Nginx配合certbot每三个月就要手工处理一次续期偶尔忘记执行命令就会导致网站证书过期。Caddy把证书申请、续期、代理转发全自动完成配置文件不到十行对自部署项目是非常友好的选型。3. 部署一套“永久在线”的DeskcommCRM完整实操3.1 服务器与基础环境准备部署DeskcommCRM建议使用一台2核4G内存以上的Linux服务器操作系统选Ubuntu 22.04 LTS或Debian 12。2核4G是底线同时跑三个容器加上日常访问会比较紧如果团队人数超过50人建议直接上4核8G。如果服务器上还有其他业务更需要留出内存余量Redis和PostgreSQL都是吃内存的大户。服务器准备好之后第一步是安装Docker和Docker Compose插件。Ubuntu系统上依次执行以下命令sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完成后确认版本docker --version和docker compose version都有输出就说明环境OK。这里有个坑很多教程里安装的docker-compose是旧版Python实现而新版Docker官方推荐的是docker compose插件两者命令中间差一个横线配置文件语法基本兼容但直接用插件更省心。服务器还有一件事要提前做配置防火墙。只放行SSH、HTTP和HTTPS三个端口其他端口默认拒绝。如果按下面示例暴露3000端口给内部调试用记得修改防火墙规则时只对指定IP放行不要把业务端口裸奔到公网。3.2 使用Docker Compose一键启动服务建一个项目目录把所有编排相关文件放在里面。我是这样组织的mkdir -p /opt/deskcommcrm cd /opt/deskcommcrm在目录下创建一个docker-compose.yml文件。下面是我当前生产环境在用的完整配置你可以直接抄过来改改就能跑version: 3.8 services: db: image: postgres:15-alpine container_name: deskcomm-db restart: always environment: POSTGRES_DB: deskcomm POSTGRES_USER: deskcomm POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - ./data/postgres:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U deskcomm] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: deskcomm-redis restart: always command: redis-server --appendonly yes volumes: - ./data/redis:/data app: image: node:20-alpine container_name: deskcomm-app restart: always working_dir: /app depends_on: db: condition: service_healthy redis: condition: service_started volumes: - ./app:/app environment: NODE_ENV: production DATABASE_URL: postgresql://deskcomm:${DB_PASSWORD}db:5432/deskcomm REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} APP_PORT: 3000 ports: - 127.0.0.1:3000:3000 command: sh -c npm install npm run start注意到几个关键点PostgreSQL容器定义了健康检查应用容器通过depends_on等待数据库准备就绪这两个配置能避免数据库还没启动完成应用就疯狂报错的问题。数据目录都映射到了宿主机./data下后续备份只需要打包这一个目录。同目录下创建一个.env文件存放敏感变量DB_PASSWORD替换成一串随机强密码 JWT_SECRET替换成一串随机字符串密码生成直接用openssl rand -base64 32就行不要手打弱密码。JWT_SECRET是用户登录会话的签名密钥泄露后任何人都能伪造登录状态这个值必须够长且只保存在.env文件中不要提交到Git仓库。最后启动sudo docker compose up -d命令执行后观察容器状态sudo docker compose ps看到三个容器状态都是Up且应用容器没有反复重启说明基础服务已经起来了。然后执行应用初始化创建数据库表结构和默认配置sudo docker compose exec app npm run migrate sudo docker compose exec app npm run create-admincreate-admin命令会提示你输入管理员邮箱和密码这一步完成之后通过http://服务器IP:3000即可打开登录页。第一次登录后系统会引导你设置企业名称、默认业务字段和销售阶段。3.3 配置域名、HTTPS与开机自启“永久在线”不只靠restart: always还要有稳定的入口和安全传输。服务器上安装Caddy之后在/etc/caddy/Caddyfile里配置一个反代规则crm.yourdomain.com { reverse_proxy 127.0.0.1:3000 }保存后执行sudo systemctl reload caddyCaddy会自动为域名申请并配置HTTPS证书后续到期也自动续签。这里唯一的前置条件是域名已经解析到服务器IP解析记录用A记录指向服务器公网地址。开机自启方面Docker服务本身默认已设为开机启动restart: always则保证容器异常退出或被系统重启时都会自动拉起。三个服务形成了完整的闭环物理服务器开机 → Docker启动 → Compose拉起所有容器 → Caddy接管域名。完成这一步之后团队所有成员通过https://crm.yourdomain.com访问系统已经具备“永久在线”的全部要素。我还额外做了一个探活脚本每5分钟请求一次登录页连续失败3次就往运维群推送告警避免页面挂了几个小时才发现。4. 员工邀请与权限体系邀请同事加入团队4.1 三种邀请成员的方式系统只有一个管理员账号显然不够用。销售团队动辄十几口人每个人都得有自己的账号和独立的客户数据视图。DeskcommCRM提供了三种添加成员的方式按场景分开讲。第一种是邀请链接。在后台“成员管理”页面点击“邀请成员”系统会生成一个有效期24小时的链接把链接发给同事对方打开后填写姓名、手机号并设置初始密码账号即生效。团队初始搭建阶段我都是复制链接到工作群里几分钟内就能把全组拉进来。需要注意邀请链接一次只能用一个生成多个链接时旧链接会失效避免被滥用。第二种是管理员主动创建账号。这种方式适合行政人员或者本来就确定好岗位角色的新员工。在成员管理里填写对方邮箱和手机号设置临时密码首次登录时会强制要求修改。主动创建的好处是不依赖对方立刻查看消息账号先挂在那里人到岗直接登录即可。第三种是批量导入。从Excel表格里整理员工姓名、邮箱、部门、角色然后按模板CSV上传一次性生成几十个账号。批量导入的动作在系统刚上线的时候特别实用几十个销售不可能一个个手动建。但注意批量导入的账号初始密码是统一的务必开启“首次登录必须修改密码”不然就是直接给不相关的人留了后门。这三种方式的适用边界日常新人入职用邀请链接临时外包人员用主动创建并勾选到期时间系统上线迁移期用批量导入。把添加成员这件事规范化之后就不容易出现“离职员工账号还在系统里”的安全隐患。4.2 角色与数据权限的精细配置员工数量多起来之后权限就成了CRM是否好用的分水岭。DeskcommCRM按“角色 数据范围”两个维度做权限控制而不是简单地给每个人一个管理员或普通用户标签。角色的设计参考了销售团队的典型岗位超级管理员、团队负责人、销售顾问、运营客服、只读访客。超级管理员拥有系统全部配置权限包括字段管理、阶段设置、成员管理和删除数据团队负责人除了自身的客户跟进还能查看本部门全部数据并做指派销售顾问只能看自己的客户和自己参与的协作客户运营客服拥有客户的只读访问权限方便跨团队查信息只读访客则连导出权限都没有适合老板或者财务做临时查看。数据范围细分为四种仅本人、本部门、全部数据、自定义范围。仅本人适合销售顾问本部门适合团队负责人全部数据留给管理员和老板账号。自定义范围是少数场景会用到的比如新产品线还在试运营只有特定几个销售能看见这条产品线的客户就用自定义范围把客户所属团队限定在指定成员之间。客户分配这个动作也要说。DeskcommCRM支持管理员或团队负责人将客户从一个销售名下转移到另一个销售名下转移时可以选择同时移交全部跟进记录。实际操作中经常遇到的情况是某位销售离职他名下的几十个客户需要批量分配给几位同事。批量转移界面里勾选人员名单系统会把客户平均分配或者按指定比例分配半天就能完成交接客户不会因为人员变动丢在数据库里变成无人跟进的死数据。还有一个容易被忽略但很重要的细节权限配置要在员工开始使用系统前做完不要先全员放开权限再一点点收紧。权限收紧的过程必然引发抱怨和适应期而一开始就定好边界后面每个人只会在自己的权限范围内做事数据质量会稳定很多。5. 日常维护与常见问题排查实录5.1 我遇到过的几个典型问题把系统上线只是第一步长期维护才是重头戏。运行这半年多我整理了六类最常遇到的问题以及对应的排查思路。现象可能原因解决思路邀请链接打开提示已失效有效期24小时已过或生成多个链接导致旧链接失效重新生成检查服务器时间是否与标准时间同步时间偏差会导致链接提前过期登录页面能打开登录后接口一直转圈后端接口报错多半是数据库连接池耗尽或Redis缓存异常查看日志docker compose logs -f app服务器重启后CRM没有恢复容器没有配置restart: always或编排文件执行时依赖顺序出错检查compose配置给所有服务都加上自动重启策略邮件跟进提醒一直收不到SMTP发信配置错误或邮箱服务商要求授权码而非登录密码检查SMTP主机、端口、加密方式和授权码用openssl s_client测试端口连通性上传客户头像或附件失败存储目录权限不足或磁盘空间已满检查挂载目录权限和df -h剩余空间早上访问系统特别慢前一天的批量数据同步导致数据库缓存失效或索引未命中为高频查询字段添加索引采用EXPLAIN分析慢查询数据库连接池耗尽是我运维早期最狼狈的一次。当时同时在线的用户量并不大但Redis里缓存了一个过期的锁导致后面所有会话都排队等待。排查了半小时最后在应用日志里看到连接等待超时重启Redis才恢复。之后我给Redis配置了持久化同时把应用里的连接池上限上调了一档。这套偶发问题最大的启发是日志和监控要在系统上线第一天就布置好。哪怕只是简单的docker compose logs --tail100和磁盘告警脚本也能把排查时间从小时级压缩到分钟级。5.2 数据备份与恢复的最终方案CRM系统里最值钱的不是代码是客户数据。我见过不止一个人系统跑了大半年从不备份直到服务器宕机才开始后悔。DeskcommCRM的备份方案我做了两重第一重是数据库层第二重是文件存储层。数据库备份用PostgreSQL自带的pg_dump写成一个定时脚本每天凌晨3点执行。脚本大致长这样#!/bin/bash BACKUP_DIR/opt/deskcomm-backup DATE$(date %Y%m%d%H%M) docker compose exec -T db pg_dump -U deskcomm deskcomm | gzip $BACKUP_DIR/db-$DATE.sql.gz find $BACKUP_DIR -name db-*.sql.gz -mtime 14 -delete脚本执行完会把14天前的备份清理掉保留最近两周的备份。不要只备份数据库本身还应该把./data目录下的上传附件一并打包。在同一个脚本里加上tar czf $BACKUP_DIR/files-$DATE.tar.gz /opt/deskcommcrm/data这样备份才完整。数据库恢复到新环境时只需要在新服务器上创建同名的数据库和账号然后用gunzip db-xxx.sql.gz | docker compose exec -T db psql -U deskcomm deskcomm导入即可。备份有一个关键注意点不要和CRM跑在同一台服务器上。我见过有人把备份目录放在系统盘同一个分区里服务器硬盘损坏时备份一起没了。靠谱的做法是备份完成后用rsync或对象存储CLI把备份文件同步到另一个存储空间哪怕只是同机房的另一台机器也远比只在本地强。恢复流程我每季度演练一次。具体是在一台临时服务器上把备份拉下来按正式流程部署新实例确认客户列表和销售看板数据一致后销毁测试环境。演练过三次确认整个恢复过程在半小时内能跑通我才真正放心地在生产账号里删过测试数据。会备份会恢复才叫数据安全只会备份不会恢复顶多算“自我安慰”。最后再分享一个我坚持至今的习惯任何数据库表结构变更比如新增字段、调整索引都先手动执行pg_dump再做。这样即使变更失败也能快速回滚到变更前状态。别笑这个习惯救过我两次一次是把某条客户记录误删一次是字段类型调整导致关联数据异常。谨慎一点CRM永远在线数据也永远有退路。
