最近总有朋友问我Docker到底是个啥,怎么用,感觉很高大上但又不知道从哪里下手。说实话,我刚开始接触Docker的时候也是一脸懵逼,各种概念搞得头大。不过用了几年下来,现在回头看,Docker真的是个好东西,能解决很多实际问题。
今天就来聊聊Docker的实际使用,不讲那些虚头巴脑的理论,直接上干货。我会把自己这几年踩过的坑、用过的技巧都分享出来,希望能帮到大家。
什么是Docker,为什么要用它
Docker说白了就是一个容器技术,可以把应用程序和它的运行环境打包在一起。你可以理解为一个轻量级的虚拟机,但比虚拟机要快很多。
我记得以前部署应用的时候,经常遇到"在我电脑上能跑啊"这种问题。开发环境Python 3.8,测试环境Python 3.7,生产环境又是3.9,各种版本冲突搞得人头疼。有了Docker之后,这些问题基本就不存在了,因为环境都打包好了,到哪里都一样。
还有一个好处就是资源利用率高。以前一台服务器可能只跑一个应用,现在可以跑十几个容器,每个容器都是独立的,互不影响。
安装Docker
安装Docker其实很简单,官网都有详细的文档。不过我还是说几个注意点。
在Ubuntu上安装:
# 更新包索引sudoaptupdate# 安装必要的包sudoaptinstallapt-transport-https ca-certificatescurlgnupg lsb-release# 添加Docker官方GPG密钥curl-fsSL https://download.docker.com/linux/ubuntu/gpg|sudogpg --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"|sudotee/etc/apt/sources.list.d/docker.list>/dev/null# 安装Docker Enginesudoaptupdatesudoaptinstalldocker-ce docker-ce-cli containerd.io安装完成后记得把当前用户加入docker组,这样就不用每次都sudo了:
sudousermod-aG docker$USER然后重新登录一下,或者执行newgrp docker。
CentOS的安装过程类似,就是包管理器换成yum。Windows和Mac的话直接下载Docker Desktop就行了,图形界面操作很方便。
Docker的基本概念
在开始实际操作之前,先理解几个核心概念。
镜像(Image):可以理解为一个模板,包含了运行应用所需的所有东西。比如一个Ubuntu镜像就包含了Ubuntu系统的基本文件。
容器(Container):镜像运行起来就是容器。一个镜像可以创建多个容器,就像一个类可以实例化多个对象一样。
仓库(Repository):存放镜像的地方,Docker Hub是最大的公共仓库。
这三个概念搞清楚了,后面的操作就好理解了。
常用Docker命令
镜像相关命令
查看本地镜像:
docker images拉取镜像:
docker pull nginx:latest docker pull ubuntu:20.04删除镜像:
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 -p8080:80 nginx:latest查看容器:
# 查看正在运行的容器dockerps# 查看所有容器(包括停止的)dockerps-a停止和启动容器:
docker stop container_id docker start container_id docker restart container_id进入容器:
dockerexec-it container_id /bin/bash删除容器:
dockerrmcontainer_id查看容器日志:
docker logs container_id docker logs -f container_id# 实时查看实战案例:部署一个Web应用
说了这么多理论,来个实际例子。我们部署一个简单的Flask应用。
首先创建一个Flask应用,文件名app.py:
fromflaskimportFlask app=Flask(__name__)@app.route('/')defhello():return'<h1>Hello from Docker!</h1>'@app.route('/health')defhealth():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 -p5000: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\-eMYSQL_ROOT_PASSWORD=mypassword\-v /opt/mysql-data:/var/lib/mysql\-p3306:3306\mysql:8.0这样即使容器删除了,数据库文件还在主机的/opt/mysql-data目录里。
Docker卷(Volume)
这是Docker推荐的方式:
# 创建卷docker volume create mydata# 使用卷docker run -d -v mydata:/data nginx:latest# 查看卷docker volumels# 查看卷详情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:latesthost模式:
容器直接使用宿主机的网络,没有网络隔离。
docker run -d --networkhostnginx:latest这种模式下容器的网络性能最好,但失去了隔离性。
none模式:
容器没有网络接口,完全隔离。
docker run -d --network none nginx:latestcontainer模式:
容器共享另一个容器的网络。
docker run -d --name web nginx:latest docker run -d --network container:web busybox:latest查看默认网络
Docker安装后会创建几个默认网络:
docker networkls通常会看到这些网络:
- bridge:默认网桥网络
- host:主机网络
- none:无网络
查看网络详情:
docker network inspect bridge这个命令会显示网络的配置信息,包括子网、网关、连接的容器等。
自定义网络
自定义网络是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 -p8080:80 nginx:latest# 映射多个端口docker run -d -p8080:80 -p8443:443 nginx:latest# 映射到指定IPdocker run -d -p127.0.0.1:8080:80 nginx:latest# 随机映射端口docker run -d -P nginx:latestUDP端口映射
# TCP端口(默认)docker run -d -p53:53 dns-server:latest# UDP端口docker run -d -p53:53/udp dns-server:latest# 同时映射TCP和UDPdocker run -d -p53:53/tcp -p53:53/udp dns-server:latest查看端口映射
# 查看容器端口映射docker port container_name# 查看所有容器的端口dockerps--format"table {{.Names}}\t{{.Ports}}"容器间通信实例
来个实际例子,部署一个Web应用,包含前端、后端、数据库:
# 创建自定义网络docker network create webapp --subnet=172.18.0.0/16# 启动数据库容器docker run -d\--name database\--network webapp\--ip172.18.0.10\-eMYSQL_ROOT_PASSWORD=rootpass\-eMYSQL_DATABASE=myapp\mysql:8.0# 启动后端API容器docker run -d\--name api\--network webapp\--ip172.18.0.20\-eDATABASE_HOST=database\-eDATABASE_PORT=3306\my-api:latest# 启动前端容器docker run -d\--name frontend\--network webapp\--ip172.18.0.30\-p80:80\-eAPI_HOST=api\-eAPI_PORT=3000\my-frontend:latest在这个例子中:
- 数据库容器不需要暴露端口到宿主机,只在内部网络中通信
- API容器可以通过"database"主机名访问数据库
- 前端容器可以通过"api"主机名访问后端
- 只有前端容器暴露端口给外部访问
网络故障排查
当容器网络出问题时,可以用这些方法排查:
检查容器网络配置
# 查看容器的网络配置docker inspect container_name|grep-A20"NetworkSettings"# 查看容器IP地址docker inspect container_name|grepIPAddress进入容器测试网络
# 进入容器dockerexec-it container_name /bin/bash# 测试网络连通性pinganother_container telnet database3306curlhttp://api:3000/health# 查看网络接口ipaddr show route -n在容器内安装网络工具
# Debian/Ubuntu容器apt-getupdate&&apt-getinstall-y iputils-pingcurltelnet net-tools# Alpine容器apkadd--no-cache iputilscurl# CentOS容器yuminstall-y iputilscurltelnet net-tools宿主机网络检查
# 查看docker网桥ipaddr show docker0 brctl show docker0# 查看iptables规则iptables -L -n iptables -t nat -L -n# 查看网络命名空间ipnetns list跨主机网络
当应用需要在多台服务器上运行时,就需要跨主机网络了。Docker提供了几种方案:
Docker Swarm网络
# 初始化Swarm集群docker swarm init --advertise-addr192.168.1.100# 在其他节点加入集群docker swarmjoin--token<token>192.168.1.100:2377# 创建overlay网络docker network create -d overlay myoverlay# 部署服务到overlay网络dockerservicecreate --network myoverlay --name web nginx:latestOverlay网络让不同主机上的容器可以直接通信,就像在同一台机器上一样。
第三方网络插件
比如Calico、Flannel、Weave等,提供更高级的网络功能。
网络安全
容器网络安全也很重要,几个建议:
网络隔离
# 创建独立的网络,避免容器间不必要的通信docker network create --internal backend-only docker run -d --network backend-only database:latest防火墙规则
# Docker会自动添加iptables规则,但可以手动调整iptables -I DOCKER-USER-s172.17.0.0/16 -d192.168.1.0/24 -j DROP使用非特权端口
# 避免使用特权端口(1-1024)docker run -d -p8080:80 nginx:latest# 好docker run -d -p80: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监控网络流量dockerexec-it container_name iftop环境变量和配置
很多应用需要通过环境变量进行配置,Docker提供了几种方式:
运行时指定
docker run -d -eMYSQL_ROOT_PASSWORD=mypassword mysql:8.0 docker run -d -eNODE_ENV=production -ePORT=3000node-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上可能需要单独安装:
sudocurl-L"https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname-s)-$(uname-m)"-o /usr/local/bin/docker-composesudochmod+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:6379depends_on:-db-redisvolumes:-./logs:/app/logsnetworks:-frontend-backenddb:image:mysql:8.0environment:MYSQL_ROOT_PASSWORD:rootpasswordMYSQL_DATABASE:myappMYSQL_USER:userMYSQL_PASSWORD:passwordvolumes:-mysql_data:/var/lib/mysqlnetworks:-backendredis:image:redis:7-alpinenetworks:-backendnginx:image:nginx:alpineports:-"80:80"volumes:-./nginx.conf:/etc/nginx/nginx.confdepends_on:-webnetworks:-frontendnetworks:frontend:driver:bridgebackend:driver:bridgeinternal:truevolumes:mysql_data:这个例子展示了网络分层:
- frontend网络连接nginx和web服务
- backend网络连接web、db和redis
- backend网络设置为internal,外部无法直接访问
使用Compose命令
# 启动所有服务docker-compose up -d# 查看服务状态docker-composeps# 查看日志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:30stimeout:10sretries:3start_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进入容器调试
# 进入运行中的容器dockerexec-it container_name /bin/bash# 如果没有bash,试试shdockerexec-it container_name /bin/sh查看容器详细信息
docker inspect container_name这个命令会输出容器的所有配置信息,包括网络、挂载点、环境变量等,调试时很有用。
容器内安装调试工具
有时候需要在容器内安装一些调试工具:
# 在Debian/Ubuntu容器内apt-getupdate&&apt-getinstall-ycurlvimnet-tools# 在Alpine容器内apkadd--no-cachecurlvim不过这种方式只是临时的,容器重启后就没了。
常见问题和解决方案
容器启动失败
首先查看日志:
docker logs container_name常见原因:
- 端口被占用
- 环境变量配置错误
- 挂载路径不存在
- 权限问题
容器内时间不对
容器默认使用UTC时间,如果需要本地时间:
docker run -d -v /etc/localtime:/etc/localtime:ro myapp:latest或者设置时区环境变量:
docker run -d -eTZ=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服务:
sudosystemctl 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-deployvariables:DOCKER_IMAGE:$CI_REGISTRY_IMAGE:$CI_COMMIT_SHAbuild:stage:buildscript:-docker build-t $DOCKER_IMAGE .-docker push $DOCKER_IMAGEtest:stage:testscript:-docker run--rm $DOCKER_IMAGE npm testdeploy:stage:deployscript:-docker pull $DOCKER_IMAGE-docker stop myapp||true-docker rm myapp||true-docker run-d--name myapp-p 80:3000 $DOCKER_IMAGEonly:-mainGitHub Actions示例
name:Build and Deployon:push:branches:[main]jobs:build-and-deploy:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v2-name:Build Docker imagerun:docker build-t myapp:${{github.sha}}.-name:Run testsrun:docker run--rm myapp:${{github.sha}}npm test-name:Deployrun:|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\-oparent=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\-oparent=eth0\-oipvlan_mode=l2\ipvlan-netIPvlan有两种模式:
- 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:
# 安装Weavesudocurl-L git.io/weave -o /usr/local/bin/weavesudochmoda+x /usr/local/bin/weave# 启动Weaveweave launcheval$(weaveenv)# 运行容器docker run -d --name web nginx:latestCalico:
# 下载Calicocurl-O -L https://github.com/projectcalico/calicoctl/releases/download/v3.24.0/calicoctlchmod+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/subuidecho'dockremap:165536:65536'>>/etc/subgid# 修改Docker配置echo'{"userns-remap": "default"}'>/etc/docker/daemon.json systemctl restart docker限制容器资源
防止容器消耗过多资源:
# 限制内存和CPUdocker run -d --memory=512m --cpus=1.0nginx:latest# 限制磁盘IOdocker run -d --device-read-bps /dev/sda:1mb --device-write-bps /dev/sda:1mb nginx:latest使用非特权容器
# 以非特权用户运行docker run -d --user1000:1000 nginx:latest# 禁用特权模式docker run -d --security-opt no-new-privileges nginx:latestAppArmor和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验证镜像:
# 启用内容信任exportDOCKER_CONTENT_TRUST=1# 推送签名镜像docker push myregistry.com/myimage:latest# 拉取时自动验证docker pull myregistry.com/myimage:latestDocker Registry私有仓库
在企业环境中,通常需要搭建私有镜像仓库。
搭建简单的Registry
# 运行Registry容器docker run -d\-p5000: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和认证:
# 生成证书mkdircerts openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days365-out certs/domain.crt# 生成认证文件mkdirauth docker run --entrypoint htpasswd registry:2 -Bbn admin password>auth/htpasswd# 运行带认证的Registrydocker run -d\-p443:5000\--restart=always\--name secure-registry\-v$(pwd)/certs:/certs\-v$(pwd)/auth:/auth\-eREGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt\-eREGISTRY_HTTP_TLS_PRIVATE_KEY=/certs/domain.key\-eREGISTRY_AUTH=htpasswd\-eREGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd\-eREGISTRY_AUTH_HTPASSWD_REALM="Registry Realm"\registry:2# 登录私有仓库docker login myregistry.comHarbor企业级仓库
Harbor是VMware开源的企业级Docker仓库:
# 下载Harborwgethttps://github.com/goharbor/harbor/releases/download/v2.8.0/harbor-offline-installer-v2.8.0.tgztarxvf harbor-offline-installer-v2.8.0.tgz# 配置Harborcdharborcpharbor.yml.tmpl harbor.ymlvimharbor.yml# 修改配置# 安装Harborsudo./install.shHarbor提供了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
# 创建manifestdocker manifest create myapp:latest\myapp:amd64\myapp:arm64\myapp:armv7# 推送manifestdocker 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/v1kind:Deploymentmetadata:name:myappspec:replicas:3selector:matchLabels:app:myapptemplate:metadata:labels:app:myappspec:containers:-name:myappimage:myregistry.com/myapp:v1.0ports:-containerPort:3000env:-name:NODE_ENVvalue:productionresources:limits:memory:512Micpu:500mrequests:memory:256Micpu:250mlivenessProbe:httpGet:path:/healthport:3000initialDelaySeconds:30periodSeconds:10readinessProbe:httpGet:path:/readyport:3000initialDelaySeconds:5periodSeconds:5镜像拉取策略
spec:containers:-name:myappimage:myapp:latestimagePullPolicy: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:latestports:-"9090:9090"volumes:-./prometheus.yml:/etc/prometheus/prometheus.yml-/var/run/docker.sock:/var/run/docker.sock:rocommand:-'--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:latestports:-"3001:3000"environment:-GF_SECURITY_ADMIN_PASSWORD=adminvolumes:-grafana-data:/var/lib/grafanavolumes:grafana-data:ELK日志收集
version:'3.8'services:elasticsearch:image:docker.elastic.co/elasticsearch/elasticsearch:8.5.0environment:-discovery.type=single-node-xpack.security.enabled=falseports:-"9200:9200"volumes:-es-data:/usr/share/elasticsearch/datalogstash:image:docker.elastic.co/logstash/logstash:8.5.0volumes:-./logstash.conf:/usr/share/logstash/pipeline/logstash.confdepends_on:-elasticsearchkibana:image:docker.elastic.co/kibana/kibana:8.5.0ports:-"5601:5601"environment:-ELASTICSEARCH_HOSTS=http://elasticsearch:9200depends_on:-elasticsearchapp: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-1ports:-"24224:24224"-"24224:24224/udp"volumes:-./fluentd.conf:/fluentd/etc/fluent.confenvironment:-FLUENTD_CONF=fluent.confapp:build:.logging:driver:"fluentd"options:fluentd-address:localhost:24224tag:docker.myappDocker性能调优
容器资源限制
合理设置资源限制,避免容器消耗过多资源:
# 内存限制docker run -d --memory=1g --memory-swap=2g nginx:latest# CPU限制docker run -d --cpus=1.5nginx:latest docker run -d --cpu-shares=512nginx: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 --filtercontainer=container_name# 进入容器调试(如果容器还在运行)dockerexec-it container_name /bin/bash# 如果容器已经退出,用同样的镜像启动一个临时容器docker run -it --rm --entrypoint /bin/bash image_name网络问题排查
# 查看容器网络配置docker inspect container_name|jq'.[0].NetworkSettings'# 测试网络连通性dockerexeccontainer_namepinggoogle.com dockerexeccontainer_namenslookupgoogle.com dockerexeccontainer_name telnet another_container3306# 查看端口监听情况dockerexeccontainer_namenetstat-tulpn dockerexeccontainer_name ss -tulpn性能问题排查
# 查看容器资源使用docker stats container_name# 查看容器进程dockerexeccontainer_namepsaux dockerexeccontainer_nametop# 查看系统调用dockerexeccontainer_namestrace-p1# 查看文件系统使用dockerexeccontainer_namedf-h dockerexeccontainer_namedu-sh /*使用dive分析镜像
dive是一个很好用的镜像分析工具:
# 安装divewgethttps://github.com/wagoodman/dive/releases/download/v0.10.0/dive_0.10.0_linux_amd64.debsudoaptinstall./dive_0.10.0_linux_amd64.deb# 分析镜像dive myapp:latestdive可以显示镜像的每一层,帮助优化镜像大小。
总结
Docker确实是个好东西,能解决很多实际问题。从开发环境的统一,到生产部署的简化,再到微服务架构的实现,都离不开容器技术。
这篇文章涵盖了Docker的方方面面,从基础概念到高级应用,从网络配置到安全实践。特别是网络部分,我花了很多篇幅来讲解,因为这确实是很多人觉得困难的地方。
网络是Docker中比较复杂的部分,但一旦理解了原理,用起来就得心应手了。记住几个要点:
- 默认bridge网络适合简单场景
- 自定义网络支持服务发现,更适合多容器应用
- 生产环境要考虑网络安全和隔离
- 跨主机网络需要用overlay或第三方插件
不过也要理性看待,Docker不是银弹,不能解决所有问题。有些场景下,传统的部署方式可能更简单直接。关键是要根据实际需求选择合适的技术。
我自己用Docker这几年,最大的感受就是它让部署变得可预测了。以前部署应用总是提心吊胆,生怕环境不一致出问题。现在有了Docker,本地测试通过的应用,到生产环境基本不会有环境问题。
当然,Docker的学习曲线还是有的,特别是网络和存储这块。但一旦掌握了,效率提升还是很明显的。建议大家从简单的应用开始,慢慢积累经验。
最后,技术是为业务服务的,不要为了用Docker而用Docker。如果现有的部署方式已经很成熟,没必要强行迁移。但如果遇到了环境不一致、部署复杂等问题,Docker确实是个不错的选择。
希望这篇文章对大家有帮助,如果觉得有用的话,别忘了点赞转发。有问题也欢迎留言讨论,大家一起学习进步。
关注@运维躬行录,我会持续分享更多实用的运维技术和经验,让我们一起在运维的路上走得更远!
公众号:运维躬行录
个人博客:躬行笔记