Baby Buddy 安装部署指南(Ubuntu 22.04 直装实录)

Ubuntu 22.04 直装 Baby Buddy v2.11.0 全过程实录:Python venv + PostgreSQL 14 + Node 构建静态资源 + Gunicorn + systemd + Nginx,含登录路由、数据库切换与构建告警等五个真实排障现场。

Baby BuddyBaby BuddyUbuntu22.04Python 编程2026/10/28

Baby Buddy 安装部署指南(Ubuntu 22.04 直装实录)

‍‍​​‌​> 本文是 Baby Buddy v2.11.0 在 Ubuntu 22.04 服务器上的直装实录:全部命令与回显均来自真机操作,无一处编造。组件定位与技术选型背景见 《Baby Buddy 介绍与选型指南》,参数级调优见 《Baby Buddy 配置参数详解》,命令级运维见 《Baby Buddy 常用 CLI 命令详解》。

一、部署架构与前置条件

Baby Buddy 是 Django 应用,单机直装的标准形态是一条三层链路:

浏览器 ──> Nginx (80, 静态资源直出 + 反向代理)
              └──> Gunicorn (127.0.0.1:8000, WSGI)
                      └──> PostgreSQL (127.0.0.1:5432)

各层职责与版本(均为本文实测环境实查值):

层组件实测版本职责
接入层Nginx1.18.0 (Ubuntu)80 端口对外;/static/、/media/ 直出;其余代理到 Gunicorn
应用层Gunicorn23.0.03 个 sync worker 绑定 127.0.0.1:8000,systemd 托管
数据层PostgreSQL14.24 (Ubuntu)业务库 babybuddy,属主 babybuddy
运行时Python3.10.12venv 隔离,Django 5.2.17 + DRF 3.18.1
构建时Node.js24.22.1仅构建期使用:npm 依赖 + gulp 编译 SCSS/JS

端口说明:本文实验服务器位于 10.89.71.0/24 网段(主机名 babybuddy-server,内网 IP 10.89.71.5),Nginx 监听 80。管理员从运维网段访问时走网关发布端口 8147(网关将 8147 映射到服务器 80),因此文中同时出现 :80(服务器本机)与 :8147(跨网段访问)两种端口的回显,属同一服务的不同访问路径。

前置条件清单:

  • Ubuntu 22.04 LTS 服务器一台,root 权限;
  • 服务器可访问外网(apt、npm、pip 均需联网;本文代理环境为 192.168.1.62:7897,无代理环境直接执行即可);
  • 磁盘 2 GB 以上(源码 + venv + node_modules + 静态资源合计约 1.2 GB)。

二、系统准备与依赖安装

2.1 系统包

一次性装齐系统依赖(PostgreSQL、Nginx、Python 编译头文件、git):

apt update
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq postgresql postgresql-contrib \
  nginx git python3 python3-venv python3-dev libpq-dev build-essential

实测回显(摘取末尾关键行):

Setting up postgresql-14 (14.24-0ubuntu0.22.04.1) ...
Setting up postgresql-contrib (14+238ubuntu1) ...
Setting up nginx (1.18.0-6ubuntu14.7) ...
Processing triggers for libc-bin (2.35-0ubuntu3.11) ...

启动数据层并确认:

systemctl start postgresql
systemctl enable postgresql nginx
psql --version
nginx -v
python3 --version

实测回显:

psql (PostgreSQL) 14.24 (Ubuntu 14.24-0ubuntu0.22.04.1)
nginx version: nginx/1.18.0 (Ubuntu)
Python 3.10.12

2.2 Node.js 24(仅构建机需要)

Ubuntu 22.04 自带的 Node 12 过老,用 NodeSource 官方源装 24.x:

curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
node -v && npm -v

实测回显:

v24.22.1
11.19.1

Node 只服务于「前端静态资源构建」这一步(第五节)。若部署机不方便装 Node,可在任一台同架构机器上构建完成后把 static/ 目录整体拷贝过来——Baby Buddy 的静态产物是自包含的(Whitenoise 带哈希清单)。

三、PostgreSQL 初始化

创建业务账号与数据库(密码用强随机串,本文示例统一 q1w2e3r4):

su postgres -c "psql -c \"CREATE USER babybuddy WITH PASSWORD 'q1w2e3r4';\""
su postgres -c "psql -c \"CREATE DATABASE babybuddy OWNER babybuddy;\""
su postgres -c "psql -c '\l' | grep babybuddy"

实测回显:

CREATE ROLE
CREATE DATABASE
 babybuddy | babybuddy | UTF8     | C.UTF-8 | C.UTF-8 |

PostgreSQL 14 默认 pg_hba.conf 对本地连接走 peer/scram-sha-256,Baby Buddy 走 TCP 127.0.0.1 连接无需改认证配置,DB_HOST=127.0.0.1 + 密码即可直连。

四、运行账户与源码部署

遵循最小权限原则:Baby Buddy 以专用系统用户运行,不赋 sudo。

useradd -r -m -d /opt/babybuddy -s /usr/sbin/nologin babybuddy

注意:-s /usr/sbin/nologin 让服务账户不可登录(安全最佳实践),代价是后面所有 manage.py 操作必须用 su -s /bin/bash babybuddy -c 方式执行。这个取舍在第十一节排障实录 #2 里有真实教训。

拉取源码并固定到 v2.11.0 tag:

cd /opt/babybuddy
git clone --branch v2.11.0 --depth 1 https://github.com/babybuddy/babybuddy.git app
chown -R babybuddy:babybuddy /opt/babybuddy

版本自证(源码内嵌版本号):

grep __version__ /opt/babybuddy/app/babybuddy/__init__.py

实测回显:

__version__ = "2.11.0"

五、Python 虚拟环境与后端依赖

su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && python3 -m venv venv"
su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && venv/bin/pip install -q -r requirements.txt"

实测关键依赖版本(venv/bin/pip list 摘取):

Django               5.2.17
djangorestframework  3.18.1
gunicorn             23.0.0
django-axes          7.1.0
django-dbsettings    1.4.0
psycopg2-binary      2.9.13
pillow               12.3.0

psycopg2-binary 已在 requirements.txt 中预置,无需单独编译;build-essential 与 libpq-dev 兜底的是个别需要源码编译的 wheel 场景。

六、前端静态资源构建(Node 24 + gulp 实录)

Baby Buddy 的 CSS/JS 图标等静态资源需要本地构建,产物落到 static/ 目录。

su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && npm ci"
su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && npx gulp build"

gulp build 等价于并行执行 extras + scripts + styles 三个子任务,实测回显:

[04:15:16] Finished 'styles' after 1.89 s
[04:15:22] Finished 'scripts' after 7.75 s
[04:15:23] Finished 'extras' after 9.23 s
[04:15:23] Finished 'build' after 9.24 s

构建过程中的真实告警(排障实录):

WARNING: 78 repetitive deprecation warnings omitted.
Run in verbose mode to see all warnings.

node_modules/bootstrap/scss/_functions.scss 217:11  @import
node_modules/bootstrap/scss/bootstrap.scss 7:9      @import
- 2:9                                               root stylesheet

解读:v2.11.0 的依赖树里 Bootstrap 5 仍以 @import 方式引用 SCSS,dart-sass 1.104 会发出大量「@import 已弃用」告警。告警可忽略——构建正常结束、产物完整。真正会让构建失败的坑有两个:

  1. 老教程装 node-sass:node-sass(libsass)不支持 Node 17+,在 Node 24 上必然编译失败。Baby Buddy v2.11.0 用的是 gulp-sass 6.x + sass(dart-sass)1.104,不要额外安装 node-sass;
  2. npm ci 与 package-lock.json 版本漂移:v2.11.0 的锁文件基于 npm 9/10 生成,npm 11 执行 npm ci 偶发 peer 依赖告警,属正常;若报 EUSAGE 不兼容,改用 npm install 后再构建。

构建完成后产物自证:

ls /opt/babybuddy/app/static/babybuddy/css | head -3

实测回显:

app.min.css
base.css
base.min.css

七、环境配置文件 .env

Baby Buddy 用 python-dotenv 自动加载工作目录下的 .env(load_dotenv(find_dotenv()),支持向上查找),全部配置项含义见 《Baby Buddy 配置参数详解》。直装最小集如下:

cat > /opt/babybuddy/app/.env <<'EOF'
DJANGO_SETTINGS_MODULE=babybuddy.settings.base
SECRET_KEY=<替换为 python3 -c "import secrets; print(secrets.token_urlsafe(48))" 的输出>
ALLOWED_HOSTS=*
DEBUG=False
TZ=Asia/Shanghai
DB_ENGINE=django.db.backends.postgresql
DB_NAME=babybuddy
DB_USER=babybuddy
DB_PASSWORD=q1w2e3r4
DB_HOST=127.0.0.1
DB_PORT=5432
EOF
chown babybuddy:babybuddy /opt/babybuddy/app/.env
chmod 600 /opt/babybuddy/app/.env

两个易混项提前说清(实测验证过的行为):

  • TZ 是操作系统时区(影响进程日志时间戳),实测 sendtestemail 的邮件头时间戳跟随它;
  • Django 的 TIME_ZONE 在 settings 里硬编码为 UTC,数据库时间统一以 UTC 存储,界面显示时区是每用户在个人设置里选的(由 UserTimezoneMiddleware 注入)。想改全站默认时区不是改 .env,详见配置篇。

八、数据库迁移、静态文件与超级用户

依次执行三连(务必以 babybuddy 身份,避免产生 root 属主文件):

cd /opt/babybuddy/app
su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && venv/bin/python manage.py migrate --noinput"

实测回显(38 个 core 迁移全过,摘取首尾):

  Applying core.0032_child_birth_time... OK
  Applying core.0033_heightpercentile_and_more... OK
  ...
  Applying core.0038_tummytime_notes... OK
  Applying sessions.0001_initial... OK

静态文件收集(Whitenoise 压缩 + 哈希):

su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && venv/bin/python manage.py collectstatic --noinput"

实测回显:

0 static files copied to '/opt/babybuddy/static', 183 unmodified, 446 post-processed.

创建超级用户(示例账号 admin / q1w2e3r4):

su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && DJANGO_SUPERUSER_PASSWORD=q1w2e3r4 venv/bin/python manage.py createsuperuser --noinput --username admin --email admin@example.com"

若此前已经跑过一次 migrate,再次执行会报 CommandError: Error: That username is already taken.——用户已存在时改用 manage.py changepassword admin 重置口令即可。

九、Gunicorn 与 systemd 服务

Gunicorn 绑定本机回环,由 Nginx 对外:

# /etc/systemd/system/babybuddy.service
[Unit]
Description=Baby Buddy (gunicorn daemon)
After=network.target

[Service]
Type=notify
User=babybuddy
Group=babybuddy
WorkingDirectory=/opt/babybuddy/app
Environment="DJANGO_SETTINGS_MODULE=babybuddy.settings.base"
ExecStart=/opt/babybuddy/app/venv/bin/gunicorn babybuddy.wsgi:application --workers 3 --bind 127.0.0.1:8000 --timeout 30 --access-logfile - --error-logfile -
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

启用并启动:

systemctl daemon-reload
systemctl enable --now babybuddy
systemctl is-active babybuddy

实测回显:

active

参数说明:

  • Type=notify:Gunicorn 23 原生支持 sd_notify,systemd 等「Gunicorn arbiter booted」才算启动成功,避免端口未就绪就被 Nginx 打穿;
  • --workers 3:官方建议 2×CPU+1,家庭规模 2~3 个足够;
  • worker 间会话缓存为默认 LocMem,无需 Redis(Baby Buddy 默认缓存走数据库表 cache_default,见配置篇)。

十、Nginx 反向代理

官方布局:静态/媒体由 Nginx 直出,动态请求代理到 Gunicorn。

# /etc/nginx/sites-available/babybuddy
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    client_max_body_size 20M;

    location /static/ {
        alias /opt/babybuddy/app/static/;
        expires 7d;
    }
    location /media/ {
        alias /opt/babybuddy/app/media/;
        expires 7d;
    }
    location / {
        proxy_pass http://127.0.0.1:8000;
        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_read_timeout 60s;
    }
}
ln -sf /etc/nginx/sites-available/babybuddy /etc/nginx/sites-enabled/babybuddy
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx

实测回显:

nginx: configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

十一、首次启动验证(登录路由实录)

登录页路由是 /login/(不是很多教程写的 /accounts/login/)。完整实测链路:

curl -s -o /dev/null -w 'root %{http_code} redirect->%{redirect_url}\n' http://127.0.0.1:8147/
curl -s -o /dev/null -w 'accounts/login %{http_code}\n' http://127.0.0.1:8147/accounts/login/
curl -s -o /dev/null -w 'login %{http_code}\n' http://127.0.0.1:8147/login/

实测回显:

root 302 redirect->http://127.0.0.1:8147/login/?next=/
accounts/login 404
login 200

带 CSRF 表单登录(浏览器无感,这里用 curl 复现完整两步):

CSRF=$(curl -s -c cj.txt http://127.0.0.1:8147/login/ | grep -oP 'csrfmiddlewaretoken" value="\K[^"]+' | head -1)
curl -s -b cj.txt -c cj.txt -o /dev/null -w 'login POST %{http_code} -> %{redirect_url}\n' \
  -d "csrfmiddlewaretoken=$CSRF&username=admin&password=q1w2e3r4&next=/" \
  -H 'Referer: http://127.0.0.1:8147/login/' http://127.0.0.1:8147/login/
curl -s -L -b cj.txt http://127.0.0.1:8147/ -o dash.html -w 'final %{url_effective} %{http_code}\n'

实测回显:

login POST 302 -> http://127.0.0.1:8147/
final http://127.0.0.1:8147/welcome/ 200

两个关键行为(都是实测得到的真实行为,别当成 bug):

  1. 登录成功 = POST /login/ 返回 302 → /,随后 sessionid 写入 cookie;
  2. 首次登录没有儿童数据时,/ 会 302 到 /welcome/——这是 Baby Buddy 的初始化向导,按页面提示创建第一个儿童档案后,/ 才会落到真正的仪表盘。创建儿童后实测:
final http://127.0.0.1:8147/children/xiaoxin-nohara/dashboard/ 200

跨网段浏览器访问 http://<网关IP>:8147/,出现深色主题登录页即部署成功。

十二、排障实录(五个真实现场)

#1 数据库初始化脚本「卡死」:把 apt 安装 + systemctl start postgresql + psql 建库 串在一条 shell 里执行时,进程停在 systemctl start postgresql 数分钟不动,且后续 CREATE USER 未执行。根因是串联脚本里用了 sudo(最小化系统未安装,报 sudo: command not found 也会挂起整条管道),加上 systemd start 与 apt 管道互相等待。处置:拆步骤执行,最小化环境用 su postgres -c 替代 sudo -u postgres;先确认 ps aux | grep systemctl 无残留再继续。

#2 su babybuddy 报 This account is currently not available:服务账户 shell 是 nologin,su babybuddy -c 直接被拒。处置:su -s /bin/bash babybuddy -c "..." 显式指定 shell。这是选择 -r 创建系统账户的必然代价。

#3 切了 PostgreSQL 却查无 auth_user 表:.env 加了 DB_* 但 psql 里 \dt 找不到 Django 表。根因:修改 .env 前应用曾以默认 SQLite 跑过 migrate(数据落在了 data/db.sqlite3),PostgreSQL 库还是空的。处置:DB_* 写好后必须 systemctl restart babybuddy 再执行 manage.py migrate;.env 是启动时一次性读入的。验证手段:

su postgres -c "psql -d babybuddy -c 'select count(*) from auth_user;'"

实测回显:

 count
-------
     1
(1 row)

#4 Host 校验 400:ALLOWED_HOSTS 收紧为具体域名后,用 IP 或错误 Host 访问一律 400 Bad Request(本文实测:Host: evil.com -> 400,Host: babybuddy.example.com -> 200)。临时排查时把 ALLOWED_HOSTS=* 后需要重启进程才生效——改配置不重启是白改。

#5 连锁失败登录被锁:连续 5 次密码错误后(AXES_FAILURE_LIMIT=5),即使密码正确也返回 429,锁定页文案为「Account locked: too many login attempts. Please try again later.」,默认冷却 1 小时(AXES_COOLOFF_TIME=1)。立即解锁:

su -s /bin/bash babybuddy -c "cd /opt/babybuddy/app && venv/bin/python manage.py axes_reset_username admin"

实测回显:

1 attempts removed.

更多命令级操作与备份恢复见 《Baby Buddy 常用 CLI 命令详解》。

十三、部署验证清单

验证项命令期望结果
数据库systemctl is-active postgresqlactive
应用systemctl is-active babybuddyactive(Status: Gunicorn arbiter booted)
接入层nginx -tsyntax is ok / test is successful
登录页curl -I http://127.0.0.1:80/login/HTTP 200
登录动作表单 POST302 → /,有 sessionid
静态资源浏览器 DevTools/static/babybuddy/css/app.*.css 200
首页登录后无数据302 → /welcome/

下一步建议:按 《Baby Buddy 配置参数详解》 收紧 ALLOWED_HOSTS、配置 HTTPS 头与 SMTP;用 《Baby Buddy 常用 CLI 命令详解》 里的 fake 命令生成演示数据熟悉界面,再为生产库制定 pg_dump 备份计划。