FieryWing 的技术博客

Docker 入门:用容器部署个人服务

2026-01-31

容器把程序和它的运行环境打包在一起,在哪台机器上都以相同方式运行,部署和迁移都更省事。我在小 VPS 上跑的几个自托管服务基本都用 Docker Compose 管理,这篇文章整理了从安装到日常维护的流程,系统是 Debian / Ubuntu,也会重点讲两个容易出安全问题的地方:端口发布和数据持久化。

几个基本概念

容器不是虚拟机,它和宿主机共用内核,启动快、占用小,但隔离性也不如虚拟机,不要把它当成绝对安全的沙箱。

安装 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 规则。所以:

  1. 默认把所有端口映射写成 127.0.0.1:宿主机端口:容器端口。
  2. 只有确实要对公网提供服务(比如不走反向代理的某个协议)才发布到 0.0.0.0,并明确知道自己在做什么。
  3. 部署后执行 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"

常见问题排查

资源限制与自启动

小内存的 VPS 上,一个失控的容器可能拖垮整台机器。可以在 Compose 里为服务设置内存上限,例如在服务下加一行 mem_limit: 512m,超限时容器会被终止而不是拖死宿主机。配合 restart: unless-stopped,服务器重启后容器会自动拉起,但前提是 Docker 服务本身开机自启:sudo systemctl enable docker。我还习惯定期用 docker stats --no-stream 看一眼各容器的资源占用,发现异常再去查日志。

小结