运维知识
悠悠
2025年12月15日

Docker从入门到放弃?不存在的!这份实战指南让你秒变容器老司机

最近总有朋友问我Docker到底是个啥,怎么用,感觉很高大上但又不知道从哪里下手。说实话,我刚开始接触Docker的时候也是一脸懵逼,各种概念搞得头大。不过用了几年下来,现在回头看,Docker真的是个好东西,能解决很多实际问题。

今天就来聊聊Docker的实际使用,不讲那些虚头巴脑的理论,直接上干货。我会把自己这几年踩过的坑、用过的技巧都分享出来,希望能帮到大家。

什么是Docker,为什么要用它

Docker说白了就是一个容器技术,可以把应用程序和它的运行环境打包在一起。你可以理解为一个轻量级的虚拟机,但比虚拟机要快很多。

我记得以前部署应用的时候,经常遇到"在我电脑上能跑啊"这种问题。开发环境Python 3.8,测试环境Python 3.7,生产环境又是3.9,各种版本冲突搞得人头疼。有了Docker之后,这些问题基本就不存在了,因为环境都打包好了,到哪里都一样。

还有一个好处就是资源利用率高。以前一台服务器可能只跑一个应用,现在可以跑十几个容器,每个容器都是独立的,互不影响。

安装Docker

安装Docker其实很简单,官网都有详细的文档。不过我还是说几个注意点。

在Ubuntu上安装:

# 更新包索引
sudo apt update

# 安装必要的包
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release

# 添加Docker官方GPG密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 设置稳定版仓库
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装Docker Engine
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io

安装完成后记得把当前用户加入docker组,这样就不用每次都sudo了:

sudo usermod -aG docker $USER

然后重新登录一下,或者执行newgrp docker

CentOS的安装过程类似,就是包管理器换成yum。Windows和Mac的话直接下载Docker Desktop就行了,图形界面操作很方便。

Docker的基本概念

在开始实际操作之前,先理解几个核心概念。

镜像(Image):可以理解为一个模板,包含了运行应用所需的所有东西。比如一个Ubuntu镜像就包含了Ubuntu系统的基本文件。

容器(Container):镜像运行起来就是容器。一个镜像可以创建多个容器,就像一个类可以实例化多个对象一样。

仓库(Repository):存放镜像的地方,Docker Hub是最大的公共仓库。

这三个概念搞清楚了,后面的操作就好理解了。

常用Docker命令

镜像相关命令

查看本地镜像:

docker images

image-20251215215521260

拉取镜像:

docker pull nginx:latest
docker pull ubuntu:20.04

image-20251215215704046

删除镜像:

docker rmi nginx:latest

构建镜像:

docker build -t myapp:v1.0 .

容器相关命令

运行容器:

# 最基本的运行
docker run hello-world

# 交互式运行
docker run -it ubuntu:20.04 /bin/bash

# 后台运行
docker run -d nginx:latest

# 端口映射
docker run -d -p 8080:80 nginx:latest

查看容器:

# 查看正在运行的容器
docker ps

# 查看所有容器(包括停止的)
docker ps -a

image-20251215215734740

停止和启动容器:

docker stop container_id
docker start container_id
docker restart container_id

进入容器:

docker exec -it container_id /bin/bash

删除容器:

docker rm container_id

查看容器日志:

docker logs container_id
docker logs -f container_id  # 实时查看

实战案例:部署一个Web应用

说了这么多理论,来个实际例子。我们部署一个简单的Flask应用。

首先创建一个Flask应用,文件名app.py:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return '<h1>Hello from Docker!</h1>'

@app.route('/health')
def health():
    return {'status': 'ok'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

创建requirements.txt:

Flask==2.3.3

然后写Dockerfile:

# 使用Python 3.9作为基础镜像
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 复制requirements文件
COPY requirements.txt .

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY app.py .

# 暴露端口
EXPOSE 5000

# 运行应用
CMD ["python", "app.py"]

构建镜像:

docker build -t flask-app:v1.0 .

运行容器:

docker run -d -p 5000:5000 --name my-flask-app flask-app:v1.0

现在访问http://localhost:5000就能看到应用了。

这个例子看起来简单,但包含了Docker的核心用法。在实际项目中,可能还需要考虑数据持久化、环境变量配置等问题。

数据持久化和卷(Volume)

容器默认是无状态的,容器删除后数据就没了。但很多时候我们需要持久化数据,比如数据库文件、日志文件等。

Docker提供了几种数据持久化的方式:

绑定挂载(Bind Mount)

直接把主机的目录挂载到容器里:

docker run -d -v /host/path:/container/path nginx:latest

比如运行一个MySQL容器,把数据目录挂载到主机:

docker run -d \
  --name mysql-server \
  -e MYSQL_ROOT_PASSWORD=mypassword \
  -v /opt/mysql-data:/var/lib/mysql \
  -p 3306:3306 \
  mysql:8.0

这样即使容器删除了,数据库文件还在主机的/opt/mysql-data目录里。

Docker卷(Volume)

这是Docker推荐的方式:

# 创建卷
docker volume create mydata

# 使用卷
docker run -d -v mydata:/data nginx:latest

# 查看卷
docker volume ls

# 查看卷详情
docker volume inspect mydata

卷的好处是由Docker管理,不用担心路径问题,而且可以在容器间共享。

临时文件系统(tmpfs)

把数据存储在内存中,容器停止后数据就没了:

docker run -d --tmpfs /tmp nginx:latest

这种方式适合存储临时数据,比如缓存文件。

Docker网络详解

Docker的网络功能很强大,也是很多人觉得复杂的地方。我刚开始学的时候也被搞得晕头转向,不过理解了原理之后就清晰多了。

Docker网络架构

Docker使用Linux的网络命名空间来实现容器网络隔离。每个容器都有自己的网络栈,包括网卡、路由表、防火墙规则等。

Docker在宿主机上创建了一个虚拟网桥docker0,默认情况下所有容器都连接到这个网桥上。容器之间可以通过这个网桥进行通信。

默认网络模式

Docker提供了几种网络模式:

bridge模式(默认)
容器连接到docker0网桥,有自己的IP地址,可以和其他容器通信。

docker run -d --name web nginx:latest

host模式
容器直接使用宿主机的网络,没有网络隔离。

docker run -d --network host nginx:latest

这种模式下容器的网络性能最好,但失去了隔离性。

none模式
容器没有网络接口,完全隔离。

docker run -d --network none nginx:latest

container模式
容器共享另一个容器的网络。

docker run -d --name web nginx:latest
docker run -d --network container:web busybox:latest

查看默认网络

Docker安装后会创建几个默认网络:

docker network ls

通常会看到这些网络:

  • bridge:默认网桥网络
  • host:主机网络
  • none:无网络

image-20251215220031477

查看网络详情:

docker network inspect bridge

这个命令会显示网络的配置信息,包括子网、网关、连接的容器等。

image-20251215220050896

自定义网络

自定义网络是Docker网络的精髓,可以实现更灵活的网络配置。

创建bridge网络

# 创建自定义网络
docker network create mynetwork

# 指定子网和网关
docker network create --driver bridge \
  --subnet=172.20.0.0/16 \
  --ip-range=172.20.240.0/20 \
  --gateway=172.20.0.1 \
  mynetwork2

使用自定义网络

# 运行容器时指定网络
docker run -d --network mynetwork --name web nginx:latest
docker run -d --network mynetwork --name db mysql:8.0

# 给运行中的容器连接网络
docker network connect mynetwork existing_container

自定义网络的优势

在自定义网络中,容器可以通过容器名相互访问,这是默认bridge网络没有的功能:

# 创建网络和容器
docker network create webapp
docker run -d --network webapp --name database mysql:8.0
docker run -d --network webapp --name backend node-app:latest
docker run -d --network webapp --name frontend nginx:latest

# backend容器可以通过"database"这个主机名访问数据库
# frontend容器可以通过"backend"访问后端API

这个功能叫做自动服务发现,非常实用。

网络连接和断开

容器可以同时连接多个网络:

# 创建两个网络
docker network create frontend
docker network create backend

# 容器连接到多个网络
docker run -d --network frontend --name web nginx:latest
docker network connect backend web

# 断开网络连接
docker network disconnect frontend web

这样可以实现更复杂的网络拓扑,比如三层架构中的网络隔离。

端口映射详解

端口映射让外部可以访问容器内的服务,有几种方式:

基本端口映射

# 映射单个端口:宿主机8080端口映射到容器80端口
docker run -d -p 8080:80 nginx:latest

# 映射多个端口
docker run -d -p 8080:80 -p 8443:443 nginx:latest

# 映射到指定IP
docker run -d -p 127.0.0.1:8080:80 nginx:latest

# 随机映射端口
docker run -d -P nginx:latest

UDP端口映射

# TCP端口(默认)
docker run -d -p 53:53 dns-server:latest

# UDP端口
docker run -d -p 53:53/udp dns-server:latest

# 同时映射TCP和UDP
docker run -d -p 53:53/tcp -p 53:53/udp dns-server:latest

查看端口映射

# 查看容器端口映射
docker port container_name

# 查看所有容器的端口
docker ps --format "table {{.Names}}\t{{.Ports}}"

容器间通信实例

来个实际例子,部署一个Web应用,包含前端、后端、数据库:

# 创建自定义网络
docker network create webapp --subnet=172.18.0.0/16

# 启动数据库容器
docker run -d \
  --name database \
  --network webapp \
  --ip 172.18.0.10 \
  -e MYSQL_ROOT_PASSWORD=rootpass \
  -e MYSQL_DATABASE=myapp \
  mysql:8.0

# 启动后端API容器
docker run -d \
  --name api \
  --network webapp \
  --ip 172.18.0.20 \
  -e DATABASE_HOST=database \
  -e DATABASE_PORT=3306 \
  my-api:latest

# 启动前端容器
docker run -d \
  --name frontend \
  --network webapp \
  --ip 172.18.0.30 \
  -p 80:80 \
  -e API_HOST=api \
  -e API_PORT=3000 \
  my-frontend:latest

在这个例子中:

  • 数据库容器不需要暴露端口到宿主机,只在内部网络中通信
  • API容器可以通过"database"主机名访问数据库
  • 前端容器可以通过"api"主机名访问后端
  • 只有前端容器暴露端口给外部访问

网络故障排查

当容器网络出问题时,可以用这些方法排查:

检查容器网络配置

# 查看容器的网络配置
docker inspect container_name | grep -A 20 "NetworkSettings"

# 查看容器IP地址
docker inspect container_name | grep IPAddress

进入容器测试网络

# 进入容器
docker exec -it container_name /bin/bash

# 测试网络连通性
ping another_container
telnet database 3306
curl http://api:3000/health

# 查看网络接口
ip addr show
route -n

在容器内安装网络工具

# Debian/Ubuntu容器
apt-get update && apt-get install -y iputils-ping curl telnet net-tools

# Alpine容器
apk add --no-cache iputils curl

# CentOS容器
yum install -y iputils curl telnet net-tools

宿主机网络检查

# 查看docker网桥
ip addr show docker0
brctl show docker0

# 查看iptables规则
iptables -L -n
iptables -t nat -L -n

# 查看网络命名空间
ip netns list

跨主机网络

当应用需要在多台服务器上运行时,就需要跨主机网络了。Docker提供了几种方案:

Docker Swarm网络

# 初始化Swarm集群
docker swarm init --advertise-addr 192.168.1.100

# 在其他节点加入集群
docker swarm join --token <token> 192.168.1.100:2377

# 创建overlay网络
docker network create -d overlay myoverlay

# 部署服务到overlay网络
docker service create --network myoverlay --name web nginx:latest

Overlay网络让不同主机上的容器可以直接通信,就像在同一台机器上一样。

第三方网络插件

比如Calico、Flannel、Weave等,提供更高级的网络功能。

网络安全

容器网络安全也很重要,几个建议:

网络隔离

# 创建独立的网络,避免容器间不必要的通信
docker network create --internal backend-only
docker run -d --network backend-only database:latest

防火墙规则

# Docker会自动添加iptables规则,但可以手动调整
iptables -I DOCKER-USER -s 172.17.0.0/16 -d 192.168.1.0/24 -j DROP

使用非特权端口

# 避免使用特权端口(1-1024)
docker run -d -p 8080:80 nginx:latest  # 好
docker run -d -p 80:80 nginx:latest    # 不推荐

网络性能优化

选择合适的网络模式

  • 对性能要求极高的应用可以使用host模式
  • 一般应用使用bridge模式就够了
  • 需要网络隔离的用自定义网络

调整网络参数

# 修改docker daemon配置
cat > /etc/docker/daemon.json << EOF
{
  "bip": "172.17.0.1/16",
  "mtu": 1500,
  "default-address-pools": [
    {
      "base": "172.80.0.0/12",
      "size": 24
    }
  ]
}
EOF

监控网络流量

# 查看容器网络统计
docker stats --format "table {{.Container}}\t{{.NetIO}}"

# 使用iftop监控网络流量
docker exec -it container_name iftop

环境变量和配置

很多应用需要通过环境变量进行配置,Docker提供了几种方式:

运行时指定

docker run -d -e MYSQL_ROOT_PASSWORD=mypassword mysql:8.0
docker run -d -e NODE_ENV=production -e PORT=3000 node-app:latest

使用env文件

创建.env文件:

MYSQL_ROOT_PASSWORD=mypassword
MYSQL_DATABASE=myapp
MYSQL_USER=appuser
MYSQL_PASSWORD=apppassword

然后运行:

docker run -d --env-file .env mysql:8.0

这种方式比较安全,敏感信息不会出现在命令行里。

在Dockerfile中设置默认值

FROM node:16
ENV NODE_ENV=production
ENV PORT=3000
# ...

Docker Compose:多容器应用的救星

当应用变复杂,需要多个容器协同工作时,一个个手动启动容器就很麻烦了。Docker Compose就是来解决这个问题的。

安装Docker Compose

现在的Docker Desktop都自带了Compose,Linux上可能需要单独安装:

sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

编写docker-compose.yml

比如一个典型的Web应用,需要Web服务器、数据库、Redis缓存:

version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=mysql://user:password@db:3306/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - ./logs:/app/logs
    networks:
      - frontend
      - backend

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: myapp
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - backend

  redis:
    image: redis:7-alpine
    networks:
      - backend

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - web
    networks:
      - frontend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

volumes:
  mysql_data:

这个例子展示了网络分层:

  • frontend网络连接nginx和web服务
  • backend网络连接web、db和redis
  • backend网络设置为internal,外部无法直接访问

使用Compose命令

# 启动所有服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看日志
docker-compose logs web
docker-compose logs -f  # 实时查看所有服务日志

# 停止服务
docker-compose stop

# 停止并删除容器
docker-compose down

# 重新构建并启动
docker-compose up -d --build

用了Compose之后,管理多容器应用就轻松多了。一个命令就能启动整个应用栈,而且服务之间的依赖关系也处理得很好。

实际生产环境的最佳实践

镜像优化

镜像大小直接影响部署速度,所以要尽量优化:

使用多阶段构建

# 构建阶段
FROM node:16 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 运行阶段
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

选择合适的基础镜像

  • alpine版本通常比较小
  • slim版本是精简版,比完整版小很多
  • 如果不需要包管理器,可以用distroless镜像

清理不必要的文件

RUN apt-get update && apt-get install -y \
    package1 \
    package2 \
    && rm -rf /var/lib/apt/lists/*

安全考虑

不要用root用户运行应用

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nextjs

扫描镜像漏洞

docker scan myapp:latest

使用.dockerignore

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.nyc_output

健康检查

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

在Compose中:

services:
  web:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

日志管理

Docker默认的json-file日志驱动会让日志文件越来越大,生产环境建议配置日志轮转:

docker run -d \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  nginx:latest

或者使用外部日志系统,比如ELK Stack。

监控和调试

查看容器资源使用情况

# 实时查看
docker stats

# 查看特定容器
docker stats container_name

进入容器调试

# 进入运行中的容器
docker exec -it container_name /bin/bash

# 如果没有bash,试试sh
docker exec -it container_name /bin/sh

查看容器详细信息

docker inspect container_name

这个命令会输出容器的所有配置信息,包括网络、挂载点、环境变量等,调试时很有用。

容器内安装调试工具

有时候需要在容器内安装一些调试工具:

# 在Debian/Ubuntu容器内
apt-get update && apt-get install -y curl vim net-tools

# 在Alpine容器内
apk add --no-cache curl vim

不过这种方式只是临时的,容器重启后就没了。

常见问题和解决方案

容器启动失败

首先查看日志:

docker logs container_name

常见原因:

  • 端口被占用
  • 环境变量配置错误
  • 挂载路径不存在
  • 权限问题

容器内时间不对

容器默认使用UTC时间,如果需要本地时间:

docker run -d -v /etc/localtime:/etc/localtime:ro myapp:latest

或者设置时区环境变量:

docker run -d -e TZ=Asia/Shanghai myapp:latest

镜像拉取慢

国内网络环境下,从Docker Hub拉取镜像可能很慢,可以配置镜像加速器。

编辑/etc/docker/daemon.json:

{
  "registry-mirrors": [
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub-mirror.c.163.com"
  ]
}

然后重启Docker服务:

sudo systemctl restart docker

容器间通信问题

确保容器在同一个网络中,或者使用link(虽然已经deprecated):

docker run -d --name db mysql:8.0
docker run -d --link db:database web-app:latest

不过建议还是用自定义网络,更灵活。

数据丢失问题

记住容器是无状态的,重要数据一定要持久化。数据库、配置文件、日志文件都要挂载到主机或者Docker卷。

Docker在CI/CD中的应用

Docker在持续集成和持续部署中发挥了重要作用。

GitLab CI示例

stages:
  - build
  - test
  - deploy

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

build:
  stage: build
  script:
    - docker build -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE

test:
  stage: test
  script:
    - docker run --rm $DOCKER_IMAGE npm test

deploy:
  stage: deploy
  script:
    - docker pull $DOCKER_IMAGE
    - docker stop myapp || true
    - docker rm myapp || true
    - docker run -d --name myapp -p 80:3000 $DOCKER_IMAGE
  only:
    - main

GitHub Actions示例

name: Build and Deploy

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
  
    steps:
    - uses: actions/checkout@v2
  
    - name: Build Docker image
      run: docker build -t myapp:${{ github.sha }} .
  
    - name: Run tests
      run: docker run --rm myapp:${{ github.sha }} npm test
  
    - name: Deploy
      run: |
        docker stop myapp || true
        docker rm myapp || true
        docker run -d --name myapp -p 80:3000 myapp:${{ github.sha }}

这样每次代码提交后,就会自动构建镜像、运行测试、部署应用。

性能优化技巧

减少镜像层数

每个RUN、COPY、ADD指令都会创建一个新的镜像层,尽量合并:

# 不好的做法
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2

# 好的做法
RUN apt-get update && apt-get install -y \
    package1 \
    package2 \
    && rm -rf /var/lib/apt/lists/*

利用构建缓存

Docker会缓存镜像层,把变化频繁的指令放在后面:

# 先复制依赖文件
COPY package.json package-lock.json ./
RUN npm ci

# 再复制源代码
COPY . .

这样源代码变化时,依赖安装的缓存还能用。

使用.dockerignore

类似.gitignore,避免把不必要的文件复制到镜像中:

node_modules
.git
.gitignore
README.md
Dockerfile
.dockerignore

选择合适的基础镜像

  • 开发环境可以用完整版镜像,方便调试
  • 生产环境用精简版,减少攻击面和镜像大小
  • 如果追求极致性能,可以用scratch或distroless

Docker网络高级配置

刚才讲了基础的网络知识,现在来看看一些高级配置。

MacVLAN网络

MacVLAN可以让容器直接获得物理网络的IP地址,就像虚拟机一样:

# 创建MacVLAN网络
docker network create -d macvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  macvlan-net

# 运行容器
docker run -d --network macvlan-net --ip=192.168.1.100 nginx:latest

这种方式的好处是容器可以直接和物理网络中的设备通信,不需要端口映射。但缺点是需要网络管理员分配IP地址,而且不是所有的网络环境都支持。

IPvlan网络

IPvlan类似MacVLAN,但使用同一个MAC地址:

docker network create -d ipvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  -o ipvlan_mode=l2 \
  ipvlan-net

IPvlan有两种模式:

  • L2模式:类似MacVLAN,但共享MAC地址
  • L3模式:路由模式,需要配置路由规则

网络别名

在自定义网络中,可以给容器设置别名:

docker network create mynet
docker run -d --network mynet --network-alias db --network-alias database mysql:8.0

这样其他容器可以通过"db"或"database"来访问这个MySQL容器。

网络插件

Docker支持第三方网络插件,比如:

Weave

# 安装Weave
sudo curl -L git.io/weave -o /usr/local/bin/weave
sudo chmod a+x /usr/local/bin/weave

# 启动Weave
weave launch
eval $(weave env)

# 运行容器
docker run -d --name web nginx:latest

Calico

# 下载Calico
curl -O -L https://github.com/projectcalico/calicoctl/releases/download/v3.24.0/calicoctl
chmod +x calicoctl

# 配置网络策略
calicoctl apply -f network-policy.yaml

这些插件提供了更高级的网络功能,比如网络策略、跨数据中心网络等。

Docker存储驱动

Docker支持多种存储驱动,不同的驱动有不同的特点:

查看当前存储驱动

docker info | grep "Storage Driver"

常见存储驱动

overlay2(推荐):

  • 性能好,功能完整
  • 支持所有Docker功能
  • 大部分Linux发行版的默认选择

aufs

  • 老的存储驱动
  • 在一些老系统上还在用
  • 性能不如overlay2

devicemapper

  • 使用块设备
  • 适合企业级存储
  • 配置复杂

btrfs

  • 支持快照和子卷
  • 适合需要高级存储功能的场景

配置存储驱动

编辑/etc/docker/daemon.json:

{
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ]
}

然后重启Docker服务。

Docker安全最佳实践

安全是生产环境必须考虑的问题,Docker提供了很多安全功能。

用户命名空间

默认情况下,容器内的root用户就是宿主机的root用户,这很危险。用户命名空间可以把容器内的root映射到宿主机的普通用户:

# 配置用户命名空间
echo 'dockremap:165536:65536' >> /etc/subuid
echo 'dockremap:165536:65536' >> /etc/subgid

# 修改Docker配置
echo '{"userns-remap": "default"}' > /etc/docker/daemon.json
systemctl restart docker

限制容器资源

防止容器消耗过多资源:

# 限制内存和CPU
docker run -d --memory=512m --cpus=1.0 nginx:latest

# 限制磁盘IO
docker run -d --device-read-bps /dev/sda:1mb --device-write-bps /dev/sda:1mb nginx:latest

使用非特权容器

# 以非特权用户运行
docker run -d --user 1000:1000 nginx:latest

# 禁用特权模式
docker run -d --security-opt no-new-privileges nginx:latest

AppArmor和SELinux

如果系统支持,可以使用AppArmor或SELinux加强安全:

# 使用AppArmor配置文件
docker run -d --security-opt apparmor:docker-default nginx:latest

# 使用SELinux标签
docker run -d --security-opt label:type:container_t nginx:latest

扫描镜像漏洞

定期扫描镜像,发现安全漏洞:

# 使用Docker官方扫描工具
docker scan nginx:latest

# 使用Clair扫描器
docker run -d --name clair-db postgres:latest
docker run -d --name clair --link clair-db:postgres quay.io/coreos/clair:latest

签名和验证镜像

使用Docker Content Trust验证镜像:

# 启用内容信任
export DOCKER_CONTENT_TRUST=1

# 推送签名镜像
docker push myregistry.com/myimage:latest

# 拉取时自动验证
docker pull myregistry.com/myimage:latest

Docker Registry私有仓库

在企业环境中,通常需要搭建私有镜像仓库。

搭建简单的Registry

# 运行Registry容器
docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name registry \
  -v registry-data:/var/lib/registry \
  registry:2

# 推送镜像到私有仓库
docker tag myapp:latest localhost:5000/myapp:latest
docker push localhost:5000/myapp:latest

# 从私有仓库拉取
docker pull localhost:5000/myapp:latest

配置HTTPS和认证

生产环境的Registry需要HTTPS和认证:

# 生成证书
mkdir certs
openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt

# 生成认证文件
mkdir auth
docker run --entrypoint htpasswd registry:2 -Bbn admin password > auth/htpasswd

# 运行带认证的Registry
docker run -d \
  -p 443:5000 \
  --restart=always \
  --name secure-registry \
  -v $(pwd)/certs:/certs \
  -v $(pwd)/auth:/auth \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_PRIVATE_KEY=/certs/domain.key \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  registry:2

# 登录私有仓库
docker login myregistry.com

Harbor企业级仓库

Harbor是VMware开源的企业级Docker仓库:

# 下载Harbor
wget https://github.com/goharbor/harbor/releases/download/v2.8.0/harbor-offline-installer-v2.8.0.tgz
tar xvf harbor-offline-installer-v2.8.0.tgz

# 配置Harbor
cd harbor
cp harbor.yml.tmpl harbor.yml
vim harbor.yml  # 修改配置

# 安装Harbor
sudo ./install.sh

Harbor提供了Web界面、用户管理、项目管理、漏洞扫描等企业级功能。

Docker多架构镜像

现在有越来越多的ARM服务器,多架构镜像变得重要了。

构建多架构镜像

# 创建builder实例
docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap

# 构建多架构镜像
docker buildx build \
  --platform linux/amd64,linux/arm64,linux/arm/v7 \
  -t myapp:latest \
  --push .

使用manifest

# 创建manifest
docker manifest create myapp:latest \
  myapp:amd64 \
  myapp:arm64 \
  myapp:armv7

# 推送manifest
docker manifest push myapp:latest

在Dockerfile中处理架构差异

FROM --platform=$BUILDPLATFORM golang:1.19 AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH

WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o myapp

FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]

Docker和Kubernetes集成

虽然Kubernetes现在支持多种容器运行时,但Docker仍然是最常用的。

准备镜像给Kubernetes使用

# 使用非root用户
FROM node:16-alpine
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN chown -R nextjs:nodejs /app
USER nextjs

EXPOSE 3000
CMD ["node", "server.js"]

Kubernetes部署文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myregistry.com/myapp:v1.0
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: production
        resources:
          limits:
            memory: 512Mi
            cpu: 500m
          requests:
            memory: 256Mi
            cpu: 250m
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5

镜像拉取策略

spec:
  containers:
  - name: myapp
    image: myapp:latest
    imagePullPolicy: Always  # 总是拉取最新镜像
    # imagePullPolicy: IfNotPresent  # 本地没有才拉取
    # imagePullPolicy: Never  # 从不拉取,只用本地镜像

容器监控和日志

生产环境中,监控和日志收集很重要。

Prometheus监控

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    labels:
      - "prometheus.io/scrape=true"
      - "prometheus.io/port=3000"
      - "prometheus.io/path=/metrics"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - /var/run/docker.sock:/var/run/docker.sock:ro
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana

volumes:
  grafana-data:

ELK日志收集

version: '3.8'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data

  logstash:
    image: docker.elastic.co/logstash/logstash:8.5.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    depends_on:
      - elasticsearch

  kibana:
    image: docker.elastic.co/kibana/kibana:8.5.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch

  app:
    build: .
    logging:
      driver: "gelf"
      options:
        gelf-address: "udp://localhost:12201"
        tag: "myapp"

volumes:
  es-data:

使用Fluentd收集日志

version: '3.8'

services:
  fluentd:
    image: fluent/fluentd:v1.14-1
    ports:
      - "24224:24224"
      - "24224:24224/udp"
    volumes:
      - ./fluentd.conf:/fluentd/etc/fluent.conf
    environment:
      - FLUENTD_CONF=fluent.conf

  app:
    build: .
    logging:
      driver: "fluentd"
      options:
        fluentd-address: localhost:24224
        tag: docker.myapp

Docker性能调优

容器资源限制

合理设置资源限制,避免容器消耗过多资源:

# 内存限制
docker run -d --memory=1g --memory-swap=2g nginx:latest

# CPU限制
docker run -d --cpus=1.5 nginx:latest
docker run -d --cpu-shares=512 nginx:latest

# 磁盘IO限制
docker run -d --device-read-bps /dev/sda:1mb nginx:latest
docker run -d --device-write-bps /dev/sda:1mb nginx:latest

优化Docker daemon

编辑/etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ],
  "default-ulimits": {
    "nofile": {
      "name": "nofile",
      "hard": 65536,
      "soft": 65536
    }
  },
  "max-concurrent-downloads": 10,
  "max-concurrent-uploads": 5
}

容器启动优化

# 使用多阶段构建减少镜像大小
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

FROM node:16-alpine
RUN apk --no-cache add dumb-init
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER node
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]

使用dumb-init作为PID 1进程,处理僵尸进程和信号转发。

故障排查和调试技巧

容器无法启动

# 查看容器日志
docker logs container_name
docker logs --details container_name

# 查看容器事件
docker events --filter container=container_name

# 进入容器调试(如果容器还在运行)
docker exec -it container_name /bin/bash

# 如果容器已经退出,用同样的镜像启动一个临时容器
docker run -it --rm --entrypoint /bin/bash image_name

网络问题排查

# 查看容器网络配置
docker inspect container_name | jq '.[0].NetworkSettings'

# 测试网络连通性
docker exec container_name ping google.com
docker exec container_name nslookup google.com
docker exec container_name telnet another_container 3306

# 查看端口监听情况
docker exec container_name netstat -tulpn
docker exec container_name ss -tulpn

性能问题排查

# 查看容器资源使用
docker stats container_name

# 查看容器进程
docker exec container_name ps aux
docker exec container_name top

# 查看系统调用
docker exec container_name strace -p 1

# 查看文件系统使用
docker exec container_name df -h
docker exec container_name du -sh /*

使用dive分析镜像

dive是一个很好用的镜像分析工具:

# 安装dive
wget https://github.com/wagoodman/dive/releases/download/v0.10.0/dive_0.10.0_linux_amd64.deb
sudo apt install ./dive_0.10.0_linux_amd64.deb

# 分析镜像
dive myapp:latest

dive可以显示镜像的每一层,帮助优化镜像大小。

总结

Docker确实是个好东西,能解决很多实际问题。从开发环境的统一,到生产部署的简化,再到微服务架构的实现,都离不开容器技术。

这篇文章涵盖了Docker的方方面面,从基础概念到高级应用,从网络配置到安全实践。特别是网络部分,我花了很多篇幅来讲解,因为这确实是很多人觉得困难的地方。

网络是Docker中比较复杂的部分,但一旦理解了原理,用起来就得心应手了。记住几个要点:

  • 默认bridge网络适合简单场景
  • 自定义网络支持服务发现,更适合多容器应用
  • 生产环境要考虑网络安全和隔离
  • 跨主机网络需要用overlay或第三方插件

不过也要理性看待,Docker不是银弹,不能解决所有问题。有些场景下,传统的部署方式可能更简单直接。关键是要根据实际需求选择合适的技术。

我自己用Docker这几年,最大的感受就是它让部署变得可预测了。以前部署应用总是提心吊胆,生怕环境不一致出问题。现在有了Docker,本地测试通过的应用,到生产环境基本不会有环境问题。

当然,Docker的学习曲线还是有的,特别是网络和存储这块。但一旦掌握了,效率提升还是很明显的。建议大家从简单的应用开始,慢慢积累经验。

最后,技术是为业务服务的,不要为了用Docker而用Docker。如果现有的部署方式已经很成熟,没必要强行迁移。但如果遇到了环境不一致、部署复杂等问题,Docker确实是个不错的选择。

希望这篇文章对大家有帮助,如果觉得有用的话,别忘了点赞转发。有问题也欢迎留言讨论,大家一起学习进步。

关注@运维躬行录,我会持续分享更多实用的运维技术和经验,让我们一起在运维的路上走得更远!

公众号:运维躬行录

个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码