这是「把自己部署到互联网上」系列的第 31 篇。
前面已经处理了服务器、域名、备案、安全边界、Docker 选型、全局 Nginx、HTTPS 和对象存储。
到这一篇,能想到的处理步骤已经都处理了,大家还有更好的见解,欢迎评论区友好交流一下。
进入主线动作:用 Docker 容器部署个人博客。
部署思路
这篇不是传统意义上的教程,而是按我的部署思路,把博客从
代码目录、环境变量、数据库、Docker Compose、启动检查,到接入全局 Nginx
的流程串起来。
具体命令要按项目调整,重点是理解一个博客项目要稳定运行,哪些东西必须放到可维护的位置。
部署过程
这一篇的目标是:把个人博客通过 Docker 和 Docker compose 跑起来,并具备后续接入域名和 HTTPS 的条件。
做完后,我应该知道项目在哪里,配置在哪里,数据在哪里,日志怎么看,出问题先查哪一层。
规划项目目录。
前面全局 Nginx 已经放在 /opt/coduty/nginx/,所以我的博客项目放到:
/opt/coduty/projects/blog/
如果要从 0 到 1 搭建项目目录,目录可以设计成下面的结构:
/opt/coduty/projects/blog/
docker-compose.yml
Dockerfile
.env
.env.example
app/
data/
logs/
backups/
不一定非得按照这样的项目结构,关键是几个边界要清楚:
docker-compose.yml负责服务编排.env负责环境变量- 代码放在项目目录或 `app/``
`data/放持久化数据logs/放日志或通过 Docker logs 查看backups/放临时备份和导出文件- 目录越早清楚,后面越不容易乱
如果代码在 Git 仓库里,可以拉到项目目录:
cd /opt/coduty/projects
git clone git@github.com:yourname/your-blog.git blog
cd blog
私有仓库需要提前配置 Git SSH Key。拉取后先看项目结构:
ls -la
重点确认是否有 Dockerfile、依赖文件、配置模板、数据库迁移脚本、启动说明、静态资源或上传目录。
如果项目已有 Dockerfile,也要检查它是否适合生产环境。
准备环境变量。
博客部署必须得参数:
数据库地址、数据库用户、密码、数据库名、三方 Secret Key、调试模式、允许访问域名、对象存储配置、邮件配置和管理员初始化信息。
这些不要硬编码在代码里,保留 .env.example 作为模板,在服务器上创建真实 .env:
APP_ENV=production
DEBUG=false
SECRET_KEY=change-me
DB_HOST=db
DB_PORT=3306
DB_NAME=blog
DB_USER=blog_user
DB_PASSWORD=change-me
DB_ROOT_PASSWORD=change-root-me
ALLOWED_HOSTS=blog.example.com
OSS_ENDPOINT=
OSS_BUCKET=
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
真实 .env 不要提交公开仓库,使用 .env 文件进行统一管理环境变量。
.gitignore 里要包含:
.env
Dockerfile
我的是 Python 项目,Dockerfile 可能类似:
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
这个这是个例子,Django、FastAPI、Flask、Halo、WordPress、Node.js 项目的启动方式都不同。
Dockerfile 的作用是固定运行环境
选择基础镜像,设置工作目录,安装依赖,复制代码,指定启动命令。
不要盲目复制别人的 Dockerfile,先确认自己的项目实际怎么启动。
docker-compose
博客通常有应用,还会有关系型数据库和非关系型数据库。示例配置如下:
services:
app:
build: .
container_name: blog-app
restart: unless-stopped
env_file:
- .env
depends_on:
- db
networks:
- blog-internal
- coduty-public
db:
image: mysql:8.0
container_name: blog-db
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- blog-db-data:/var/lib/mysql
networks:
- blog-internal
volumes:
blog-db-data:
networks:
blog-internal:
coduty-public:
external: true
这是个例子,供大家参考。
重点看下它的结构:
- 应用和数据库在
blog-internal通信 - 只有应用加入
coduty-public,让全局 Nginx 能访问 - 数据库不加入公共网络,也不映射公网端口
container_name便于示例说明- 实际多项目里也可以使用 network alias 来减少命名冲突。
建议不要把数据库端口暴露出去:
ports:
- "3306:3306"
应用容器访问数据库不需要宿主机端口映射,直接使用服务名即可:
DB_HOST=db
如果要用本地工具连数据库,优先考虑 SSH 隧道,不要把数据库端口暴露公网。
启动前检查
启动前先检查几件事。
公共网络是否存在:
docker network ls | grep coduty-public
不存在就创建:
docker network create coduty-public
确认 .env 存在,关键变量齐全。
再检查 Compose 解析结果:
docker compose config
同时确认项目没有直接映射 80 和 443,这两个端口应该由全局 Nginx 管。
确认后启动:
docker compose up -d --build
docker compose ps
docker compose logs -f app
如果数据库启动慢,应用可能一开始连接失败。
depends_on 只保证启动顺序,不保证数据库已经可用。
要看项目是否有重试机制,或者是否需要等待数据库就绪。
应用启动失败时先看日志,常见原因包括数据库连接失败、环境变量缺失、依赖安装失败、启动命令错误、数据库迁移未执行。
数据库初始化和迁移
不同框架方式不同。我这里用的是 Django :
docker compose exec app python manage.py migrate
docker compose exec app python manage.py createsuperuser
其他框架具体看项目文档。
我习惯把这些命令记录到 README.md 里面,并区分“第一次部署初始化”和“每次更新可能需要的迁移”。
这两类操作不要混在一起。
增加 Nginx 配置
接入全局 Nginx 前,先确认应用在内部可访问。
因为我的设计是不让应用直接暴露公网,所以不能用公网 IP 加端口访问应用。
可以进入全局 Nginx 容器测试:
docker exec -it global-nginx sh
wget -qO- http://blog-app:8000
如果失败,检查 blog-app 是否运行,是否加入 coduty-public,名称是否写对,应用是否监听 0.0.0.0,端口是否正确。
确认内部访问正常后,增加全局 Nginx 配置:
/opt/coduty/nginx/conf.d/blog.example.com.conf
示例:
server {
listen 80;
server_name blog.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name blog.example.com;
ssl_certificate /etc/nginx/certs/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/example.com/privkey.pem;
location / {
proxy_pass http://blog-app: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;
}
}
server_name 写博客域名,证书路径写容器内路径,proxy_pass 写 Nginx 容器能访问的内部服务地址。
修改后先检查:
docker exec global-nginx nginx -t
通过后再重载:
docker exec global-nginx nginx -s reload
配置 DNS
如果还没配置 DNS,把 blog.example.com 解析到服务器公网 IP。
然后检查:
nslookup blog.example.com
dig blog.example.com
访问链路要从外到内看:
DNS -> 安全组 -> 防火墙 -> 全局 Nginx -> 博客应用 -> 数据库
最后验证:
curl -I https://blog.example.com
如果返回 200、301、302 等符合预期的状态码,说明 Nginx 至少在响应。
502 通常表示请求到了 Nginx,但后端应用连接失败。此时看 Nginx 日志和应用日志:
docker logs global-nginx
docker compose logs -f app
如果完全访问不到,优先查 DNS、安全组、防火墙和端口监听,不要一开始就怀疑应用代码。
小结:常见部署问题
在部署过程中常见遇到的问题如下:
- 502 Bad Gateway,多半是后端服务不可达,重点查应用容器状态、监听端口、
proxy_pass、公共网络和监听地址 - 数据库连接失败,常见原因是
DB_HOST写成localhost。在 Compose 网络里,localhost指应用容器自己,应该写数据库服务名db - 静态资源访问不到,可能是构建或收集步骤缺失、资源路径错误、对象存储配置错误、混合内容或上传目录没有持久化
- 容器重建后数据丢失,说明数据库、上传文件或关键数据没有放到 volume、绑定目录、备份或对象存储里
下一篇继续写部署后的真实问题:博客部署后,我遇到的数据库、迁移和管理员初始化问题。
容器跑起来只是第一步,文章数据结构正确、后台能登录、文章能发,博客才算部署完成。
本文由 楸木 原创,转载请注明出处。