Bitpoll 0.11.0 Compose 部署实战

Bitpoll 0.11.0 三服务 Compose 编排部署全流程:官方基线补数据库与健康检查、settings 与 nginx 反代配置、启动/登录/投票全链路实测回显、持久化验证与五则排障实录。

Bitpoll自托管DjangoPostgreSQLPython2026/11/01

Bitpoll 0.11.0 Compose 部署实战

目标:在一台 Linux 服务器上,用 Compose 把 Bitpoll 0.11.0 以「数据库 + 应用 + 反向代理」三服务形态完整拉起,完成配置初始化、超级管理员创建、投票创建与访问的全链路验证。全部命令与回显均为 2026-11-01 在 Ubuntu 22.04 服务器实测原文照抄(凭据按本文示例值书写)。项目背景与选型论证见《Bitpoll 0.11.0 介绍与选型指南》。

一、环境信息

项值
操作系统Ubuntu 22.04(x86_64)
软件版本Bitpoll 0.11.0(官方最新稳定版,2026-05-06 发布,来源 GitHub releases.atom,查询日期 2026-11-01)
编排工具Docker Compose v2(docker compose 子命令形态)
对外 Web 端口8136(ss -ltn 实测空闲后取用)
应用调试端口8137 → 服务内部 3009(仅排障用,生产可关)
自定义网络bitpoll-net,显式子网 10.89.57.0/24
数据库PostgreSQL 17(独立服务,bind mount 数据目录 ./data/postgres)
应用服务官方发行产物 ghcr.io/fsinfuhh/bitpoll:latest(0.11.0 对应发布线)
反向代理nginx 1.23,uwsgi 协议转发到应用服务 3008 端口
管理员账号admin / q1w2e3r4(示例值,密码务必替换)

架构要点:Bitpoll 应用进程通过 uWSGI 暴露 uwsgi 协议端口 3008(生产路径)与 HTTP 端口 3009(开发路径)。对外只应暴露 nginx 的 80(映射 8136);3009 映射到 8137 仅为部署期验证,稳定后建议从编排文件删除。

二、官方基线与必要适配

官方仓库根目录提供 compose.yaml,内容仅有两个服务:

services:
  bitpoll:
    build:
      dockerfile: Dockerfile
      context: .
    ports:
     - 3008:3008
     - 3009:3009
    volumes:
     - ./run/static:/opt/static
     - ./run/config:/opt/config
     - ./run/log/:/opt/log
  web:
    image: nginx:1.23
    volumes:
     - ./docker_files/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
     - ./run/static:/opt/static
    ports:
     - "80:80"

直接照抄这份基线跑不起来,原因与适配动作:

  1. 没有数据库服务:官方假设数据库外部提供。本文补一个 postgres:17-alpine 服务,并配 pg_isready 健康检查 + depends_on.condition: service_healthy,避免应用先于数据库就绪而崩溃重启。
  2. 没有固定项目名:补 name: bitpoll-0.11.0-stack,防止与其他项目的同名目录互相误停。
  3. 端口写死 80:与服务器占用冲突,改映射 8136;3009 改 8137。
  4. 默认网络无子网规划:显式指定 10.89.57.0/24(先 docker network ls 核对 10.89 段已占槽位后取空闲)。
  5. 镜像 tag 用 latest 不可控:本文沿用 ghcr.io/fsinfuhh/bitpoll:latest 与 0.11.0 发布线对应(官方镜像由 master 自动构建),升级时以重新拉取 + 数据备份为前提。

三、目录规划

部署根目录 /opt/apps/bitpoll(实测目录为工作目录下 stack/,结构一致):

stack/
├── compose.yaml          # 编排文件(见下)
├── keys.env              # 密钥类环境变量(SECRET_KEY 等,600 权限)
├── nginx/
│   └── bitpoll.conf      # 反代配置
├── run/
│   ├── config/
│   │   └── settings.py   # Django 本地配置(挂载进应用 /opt/config/settings.py)
│   ├── static/           # 静态资源目录(应用启动时自动填充,nginx 共享读取)
│   └── log/              # 应用日志
└── data/
    └── postgres/         # 数据库数据目录(bind mount,升级/重编排不丢数据)

建目录与密钥生成实测:

mkdir -p run/{config,static,log} nginx data/postgres
python3 -c "from django.core.management.utils import get_random_secret_key as k; print('SECRET_KEY='+k())" >> keys.env
openssl rand -base64 32 | sed 's/^/FIELD_ENCRYPTION_KEY=/' >> keys.env

SECRET_KEY 是 Django 会话签名根密钥,FIELD_ENCRYPTION_KEY 用于加密存储的敏感字段(如受邀者邮箱),两者泄露等同于数据泄露,keys.env 与 run/config/settings.py 都要控制读权限。

四、配置文件

4.1 run/config/settings.py

应用启动时把 /opt/config/settings.py 软链为 Django 的 settings_local.py 覆盖默认配置。最小可用配置(实测生效原文):

# Bitpoll local settings (mounted to /opt/config/settings.py)
import os

SECRET_KEY = '8Se#-M#uT_IDttoPOqcIHi#yryr7bhPeWe-qcBC3^g7Mw_YFJC'
FIELD_ENCRYPTION_KEY = 'l4T6gZ3GO33bLuQNAhsNzrMlF+D8OVP5Aw2C3ftYL0s='

DEBUG = False
ALLOWED_HOSTS = ['*']

SITE_NAME = 'Bitpoll'
REGISTER_ENABLED = True
BASE_URL = 'http://127.0.0.1:8136'

TIME_ZONE = 'Asia/Shanghai'
LANGUAGE_CODE = 'en-us'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'bitpoll',
        'USER': 'bitpoll',
        'PASSWORD': 'bitpoll',
        'HOST': 'db',
        'PORT': '5432',
    }
}

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'bitpoll@quzuo.net'

PIPELINE_LOCAL = {}
INSTALLED_APPS_LOCAL = []

PUBLIC_POLLS = True

要点:HOST 必须写数据库服务名 db(Compose 网络内 DNS);ALLOWED_HOSTS 至少包含对外域名或本机地址,否则直接 400;生产建议收紧为白名单。配置面逐项详解见本专栏《Bitpoll 0.11.0 配置参数详解》。

4.2 nginx/bitpoll.conf

与官方 docker_files/nginx/nginx.conf 一致(实测原文):

server {
    listen 80;
    server_name default_server;

    root /opt/static;

    location / {
        include uwsgi_params;
        uwsgi_pass bitpoll:3008;
    }

    location /static {
        expires 14d;
        alias /opt/static;
    }

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
}

uwsgi_pass bitpoll:3008 指向应用服务的 uwsgi 端口;/static 直接由 nginx 从共享目录 run/static 吐出,不经应用进程。

4.3 compose.yaml(最终版)

name: bitpoll-0.11.0-stack

services:
  db:
    image: postgres:17-alpine
    container_name: bitpoll-0.11.0-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: bitpoll
      POSTGRES_USER: bitpoll
      POSTGRES_PASSWORD: bitpoll
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U bitpoll -d bitpoll"]
      interval: 5s
      timeout: 3s
      retries: 12
    networks:
      - bitpoll-net

  bitpoll:
    image: ghcr.io/fsinfuhh/bitpoll:latest
    container_name: bitpoll-0.11.0-app
    restart: unless-stopped
    volumes:
      - ./run/static:/opt/static
      - ./run/config:/opt/config
      - ./run/log:/opt/log
    ports:
      - "8137:3009"
    depends_on:
      db:
        condition: service_healthy
    networks:
      - bitpoll-net

  web:
    image: nginx:1.23
    container_name: bitpoll-0.11.0-web
    restart: unless-stopped
    volumes:
      - ./nginx/bitpoll.conf:/etc/nginx/conf.d/default.conf
      - ./run/static:/opt/static
    ports:
      - "8136:80"
    depends_on:
      - bitpoll
    networks:
      - bitpoll-net

networks:
  bitpoll-net:
    ipam:
      config:
        - subnet: 10.89.57.0/24

五、启动全流程与实测回显

5.1 端口占用自查

ss -ltn | grep -E '813[0-9]'

实测回显(本机 8132/8134/8135/8137 已被其他服务占用,8136 空闲,故 Web 取 8136):

LISTEN  0  4096  0.0.0.0:8132  0.0.0.0:*
LISTEN  0  4096  0.0.0.0:8134  0.0.0.0:*
LISTEN  0  4096  0.0.0.0:8135  0.0.0.0:*

5.2 拉起

docker compose up -d

实测回显(数据库健康检查通过后应用才启动,Waiting → Healthy 两行即 depends_on 生效的证据):

 Network bitpoll-0110-stack_bitpoll-net Creating 
 Network bitpoll-0110-stack_bitpoll-net Created 
 Container bitpoll-0.11.0-db Creating 
 Container bitpoll-0.11.0-db Created 
 Container bitpoll-0.11.0-app Creating 
 Container bitpoll-0.11.0-app Created 
 Container bitpoll-0.11.0-web Creating 
 Container bitpoll-0.11.0-web Created 
 Container bitpoll-0.11.0-db Starting 
 Container bitpoll-0.11.0-db Started 
 Container bitpoll-0.11.0-db Waiting 
 Container bitpoll-0.11.0-db Healthy 
 Container bitpoll-0.11.0-app Starting 
 Container bitpoll-0.11.0-app Started 
 Container bitpoll-0.11.0-web Starting 
 Container bitpoll-0.11.0-web Started 

5.3 状态确认

docker compose ps

实测回显:

NAME                 IMAGE                             COMMAND                  SERVICE   CREATED         STATUS                   PORTS
bitpoll-0.11.0-app   ghcr.io/fsinfuhh/bitpoll:latest   "/bin/sh -c /usr/loc…"   bitpoll   6 seconds ago   Up Less than a second    3008/tcp, 0.0.0.0:8137->3009/tcp, [::]:8137->3009/tcp
bitpoll-0.11.0-db    postgres:17-alpine                "docker-entrypoint.s…"   db        6 seconds ago   Up 5 seconds (healthy)   5432/tcp
bitpoll-0.11.0-web   nginx:1.23                        "/docker-entrypoint.…"   web       6 seconds ago   Up Less than a second    0.0.0.0:8136->80/tcp, [::]:8136->80/tcp

5.4 应用启动日志

docker compose logs --tail=12 bitpoll

实测回显(官方入口脚本先执行 manage.py migrate 建表,再发布静态资源,最后拉起 uWSGI):

bitpoll-0.11.0-app  | Set PythonHome to /opt/bitpoll/.venv
bitpoll-0.11.0-app  | Python main interpreter initialized at 0x788dd2c34390
bitpoll-0.11.0-app  | python threads support enabled
bitpoll-0.11.0-app  | your server socket listen backlog is limited to 100 connections
bitpoll-0.11.0-app  | your mercy for graceful operations on workers is 60 seconds
bitpoll-0.11.0-app  | mapped 938592 bytes (916 KB) for 32 cores
bitpoll-0.11.0-app  | *** Operational MODE: preforking+threaded ***
bitpoll-0.11.0-app  | WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x788dd2c34390 pid: 7 (default app)
bitpoll-0.11.0-app  | *** uWSGI is running in multiple interpreter mode ***
bitpoll-0.11.0-app  | spawned uWSGI master process (pid: 7)
bitpoll-0.11.0-app  | spawned uWSGI worker 1 (pid: 11, cores: 4)
bitpoll-0.11.0-app  | spawned uWSGI worker 2 (pid: 15, cores: 4)

六、数据库初始化验证

迁移与建表由应用入口脚本自动完成,无需手工干预。验证表结构就绪:

docker compose exec -T db psql -U bitpoll -d bitpoll -c "select count(*) from django_migrations;"

实测回显:

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

64 条迁移记录全部落库。业务表清单(\dt 节选):poll_poll、poll_choice、poll_choicevalue、poll_vote、poll_comment、base_bitpolluser、caldav_davcalendar 等 23 张。

七、管理员初始化

应用不提供自动建管理员,需进应用服务执行 Django 命令:

docker compose exec bitpoll python3 /opt/bitpoll/manage.py createsuperuser

按提示输入用户名 admin、邮箱 admin@quzuo.net、密码 q1w2e3r4(交互回显与更多管理命令见本专栏《Bitpoll 0.11.0 常用 CLI 命令详解》)。

八、全链路验证

8.1 首页可达

curl -sI http://127.0.0.1:8136/ | head -6
curl -s http://127.0.0.1:8136/ | grep -oE '<title>[^<]*</title>'

实测回显:

HTTP/1.1 200 OK
Server: nginx/1.23.4
Date: Sun, 01 Nov 2026 11:53:20 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 9741
Connection: keep-alive
<title>Create poll - Bitpoll</title>

首页即「创建投票」页(未登录可匿名建投票,由 PUBLIC_POLLS/注册开关控制)。响应头里 Content-Security-Policy-Report-Only、X-Frame-Options: DENY 等安全头由 django-simple-csp 中间件自动注入。

8.2 登录 admin

TOKEN=$(curl -s -c cookies.txt "http://127.0.0.1:8136/login/" | grep -oE 'csrfmiddlewaretoken" value="[^"]+' | head -1 | cut -d'"' -f3)
curl -s -b cookies.txt -c cookies.txt -e "http://127.0.0.1:8136/login/" \
  -d "csrfmiddlewaretoken=$TOKEN&username=admin&password=q1w2e3r4&next=/polls/manage" \
  -o /dev/null -w "login HTTP %{http_code} redirect=%{redirect_url}\n" \
  "http://127.0.0.1:8136/login/"

实测回显:

login HTTP 302 redirect=http://127.0.0.1:8136/polls/manage

8.3 登录态确认

curl -s -b cookies.txt http://127.0.0.1:8136/settings/ | grep -oE '<title>[^<]*</title>'

实测回显:

<title>Settings - Bitpoll</title>

/settings/ 需登录态才能访问,返回 200 即会话生效。

8.4 投票创建与访问

以登录态 POST 创建投票(表单流程与 Web 界面一致),成功后 302 进入选项编辑页:

HTTP/1.1 302 Found
Location: /poll/sprint-review/edit/choices/universal/

随后访问投票页验证:

curl -s http://127.0.0.1:8136/poll/sprint-review/ | grep -oE '<title>[^<]*</title>'

实测回显:

<title>Sprint 回顾表决 - Bitpoll</title>

投票支持中文标题,时间戳、选项、投票人矩阵渲染正常;/poll/<url>.csv 可直接导出结果。

九、持久化与重启验证

docker compose down 后再 up -d,数据库与投票数据完整保留(bind mount 目录 data/postgres 不随编排销毁):

docker compose down
docker compose up -d
curl -s http://127.0.0.1:8136/poll/weekly-meeting/ | grep -oE '<title>[^<]*</title>'

实测回显:

<title>项目周会时间投票 - Bitpoll</title>

restart: unless-stopped 保证服务器重启后三服务自动拉起;静态目录 run/static 每次应用启动时由入口脚本清空重灌,无需人工维护。

十、备份与升级

  • 备份:docker compose exec -T db pg_dump -U bitpoll bitpoll > backup.sql(实测产物 100KB 级),连同 run/config/、keys.env 一起归档。恢复用 psql -U bitpoll -d bitpoll < backup.sql。
  • 升级:先备份,再 docker compose pull && docker compose up -d;入口脚本自动跑增量迁移。回滚即恢复旧备份 + 指定旧发布线重新拉起。

十一、排障实录

  1. 照抄官方 compose 起不来:官方 compose.yaml 无数据库服务,应用启动即报 OperationalError: could not connect to server 并循环重启。解决:按本文第二节补 db 服务与 depends_on: condition: service_healthy。
  2. 登录 POST 返回 404:页面模板里的 /account/login/ 只是注册流程的中转路径,对它 POST 会 404;真正的登录端点是站点根下 /login/(Django LoginView 挂载点)。用 curl -d ... http://127.0.0.1:8136/login/ 后返回 302,会话建立。
  3. 首页 400 DisallowedHost:settings.py 未配 ALLOWED_HOSTS 时,通过 IP+端口访问被 Django 拒绝。解决:ALLOWED_HOSTS = ['*'](临时)或写入实际域名/IP 白名单(生产)。
  4. 静态资源 404 页面裸奔:run/static 目录未挂载给 nginx 服务,或应用还没跑过一次启动(静态文件由入口脚本从应用包内复制到 run/static)。确认两个服务共享同一 ./run/static 目录,且应用启动日志无报错后刷新即可。
  5. 数据库就绪竞态:去掉健康检查后应用先于数据库完成初始化启动,报连接错误。解决:保留 pg_isready 健康检查 + service_healthy 条件,本文 5.2 回显中 Waiting → Healthy 即该机制生效。

十二、收尾清单

  • settings.py 中 ALLOWED_HOSTS 收紧为实际域名
  • BASE_URL 改为对外正式地址(影响通知邮件里的链接)
  • EMAIL_BACKEND 从 console 换为真实 SMTP
  • 删除 8137:3009 端口映射(开发端口不对外)
  • keys.env、run/config/、data/ 权限收敛(600/700,属主统一)
  • 配置 data/postgres 与 backup.sql 的定期异地备份

部署完成后,配置项逐项含义见《Bitpoll 0.11.0 配置参数详解》,日常运维命令与 API 实操见《Bitpoll 0.11.0 常用 CLI 命令详解》。

Bitpoll 0.11.0 Compose 部署实战 - Quzuo