Docker 入门:用容器部署个人服务
2026-01-31
容器把程序和它的运行环境打包在一起,在哪台机器上都以相同方式运行,部署和迁移都更省事。我在小 VPS 上跑的几个自托管服务基本都用 Docker Compose 管理,这篇文章整理了从安装到日常维护的流程,系统是 Debian / Ubuntu,也会重点讲两个容易出安全问题的地方:端口发布和数据持久化。
几个基本概念
- 镜像:只读的程序模板。
- 容器:镜像运行起来的实例。
- 数据卷:把数据保存在容器之外,容器删了数据还在。
- 网络:容器之间通过 Docker 网络互相访问,可以用服务名当主机名。
- Compose:用一个 YAML 文件描述多个容器及其关系,一条命令启停。
容器不是虚拟机,它和宿主机共用内核,启动快、占用小,但隔离性也不如虚拟机,不要把它当成绝对安全的沙箱。
安装 Docker
推荐按 Docker 官方文档添加官方 apt 源安装 Docker Engine 和 Compose 插件,这样能获得较新的版本。具体步骤会随版本变化,请以官方文档为准。装好之后验证:
sudo docker version sudo docker compose version sudo docker run --rm hello-world
很多教程会让你把用户加入 docker 组以免每次写 sudo。需要知道的是,docker 组成员实际上等同于拥有 root 权限,所以只给你信任的账号。在个人服务器上我选择继续写 sudo,麻烦一点但更稳妥。
常用命令
docker run -d --name web -p 127.0.0.1:8080:80 nginx docker ps docker logs web docker stop web
这几条的含义:-d 后台运行,--name 指定名字,-p 做端口映射,格式是“宿主机地址:宿主机端口:容器端口”。另外几个也常用:
docker ps -a docker logs -f --tail 100 web docker exec -it web sh docker rm web docker images docker image prune
docker exec -it 可以进入运行中的容器调试;docker rm 删除已停止的容器;docker image prune 清理没用的镜像。执行任何 prune 之前,请先想清楚会删掉什么。
用 Compose 管理
services:
app:
image: nginx
ports:
- "127.0.0.1:8080:80"
restart: unless-stopped
然后 docker compose up -d 启动。端口映射前面加上 127.0.0.1,可以避免服务直接暴露在公网,再让反向代理对外提供 HTTPS,配置见《Caddy 入门》。
Compose 文件一般放在专门的目录,例如 ~/apps/blog/compose.yaml,常用命令都在该目录下执行:
docker compose up -d docker compose ps docker compose logs -f docker compose pull docker compose down
down 会停止并删除容器和默认网络,但不会删除命名数据卷;只有加上 -v 才会连卷一起删,这个选项要慎用。
一个更完整的例子:应用加数据库
下面是一个带数据库的示例,演示数据卷、环境变量和服务间通信。镜像名和密码都是占位,实际使用请换成自己的并妥善保管密码:
services:
app:
image: example/app:1.0
ports:
- "127.0.0.1:8080:3000"
environment:
DB_HOST: db
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- db-data:/var/lib/postgresql/data
restart: unless-stopped
secrets:
db_password:
file: ./db_password.txt
volumes:
db-data:
要点有三个。第一,数据库没有写 ports,所以不会发布到宿主机,只有同一网络里的 app 能通过服务名 db 访问。第二,密码放在单独文件里,通过 secrets 传入,文件权限设为只有自己可读(chmod 600 db_password.txt),并且不要提交到 Git。第三,数据放在命名卷 db-data 中,删掉容器数据不会丢。镜像是否支持 _FILE 这类变量要看具体镜像的文档,不支持就改用环境变量或 env 文件,同样要保护好文件权限。
数据持久化与备份
容器内部的文件系统是临时的,容器重建就没了。需要保存的数据要么挂载命名卷,要么挂载宿主机目录(bind mount):
volumes:
- ./data:/app/data
bind mount 的好处是路径明确,便于备份;坑在于文件属主和权限,容器里的用户 ID 和宿主机不一致时会出现“没有权限写入”。数据库类的数据,建议用数据库自带的导出工具备份,而不是直接复制正在运行的数据文件。完整的备份和还原思路见《服务器备份与基础监控》。
注意:Docker 会绕过 ufw
Docker 发布的端口可能绕过 ufw 规则,要特别留意监听地址。原因是 Docker 自己往 iptables 里写转发规则,流量不经过 ufw 的 INPUT 规则。所以:
- 默认把所有端口映射写成
127.0.0.1:宿主机端口:容器端口。 - 只有确实要对公网提供服务(比如不走反向代理的某个协议)才发布到 0.0.0.0,并明确知道自己在做什么。
- 部署后执行
sudo ss -ltnp,核对每个端口的监听地址,不要只看ufw status。
防火墙整体的设置见《Linux 防火墙入门》。
更新与清理
更新某个服务的流程是:先备份数据,然后在 Compose 目录下执行
docker compose pull docker compose up -d docker image prune
镜像标签尽量固定到明确的版本,不要长期使用 latest,否则某次 pull 之后版本意外跳变,可能引入不兼容。升级大版本之前先阅读该项目的升级说明,特别是数据库类镜像,跨大版本通常需要迁移数据。
日志也会占磁盘。默认的日志驱动可能让日志文件无限增长,可以在服务里限制大小:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
常见问题排查
- 容器反复重启:
docker logs 容器名看报错,多半是配置或权限问题。 - 端口访问不到:确认映射写对了,容器内程序监听的是 0.0.0.0 而不是容器内的 127.0.0.1,否则宿主机映射过去也连不上。
- 磁盘满了:
docker system df查看占用,再决定清理哪些。 - YAML 报错:缩进只能用空格,不能用 Tab;先用
docker compose config检查并展开配置。 - 服务之间连不上:确认在同一个 Compose 网络里,用服务名而不是 localhost。
资源限制与自启动
小内存的 VPS 上,一个失控的容器可能拖垮整台机器。可以在 Compose 里为服务设置内存上限,例如在服务下加一行 mem_limit: 512m,超限时容器会被终止而不是拖死宿主机。配合 restart: unless-stopped,服务器重启后容器会自动拉起,但前提是 Docker 服务本身开机自启:sudo systemctl enable docker。我还习惯定期用 docker stats --no-stream 看一眼各容器的资源占用,发现异常再去查日志。
小结
- 用 Compose 文件描述服务,配置可版本化、可复现。
- 端口映射默认绑定 127.0.0.1,再用反向代理提供 HTTPS。
- 数据放在卷或 bind mount 里,并纳入备份。
- 密码不要写进镜像或提交到仓库,固定镜像版本,升级前先备份。