1. 先想清楚:Docker解决的是"环境搬运"这件事
1.1 从"在我电脑上是好的"说起
在聊Docker的具体命令之前,我想先花点时间把"为什么要容器化"这件事说透。很多新手一上来就急着敲 docker run,跑到一半遇到各种环境问题,然后就放弃了。其实Docker真正解决的是环境不一致的问题:你在开发环境里跑得好好的,到测试环境、生产环境就起不来,数据库连不上、依赖缺失、版本冲突,最后只能无奈地说"在我电脑上是好的"。
Docker把应用代码和它依赖的运行时、配置、库文件打包成一个标准化的镜像,然后在任何装了Docker的机器上用同一个镜像启动容器。容器不是虚拟机,它共享宿主机的内核,但拥有独立的文件系统、进程、网络和用户空间。所以同一个MySQL 8.0镜像,在你的Windows上用,在云服务器Linux上用,跑出来的行为基本一致,差别最多是映射端口、数据卷位置这些外部配置。
对于刚入门的人来说,这种"环境搬运"的能力意味着你不需要再为每个项目手动安装一堆依赖,也不需要担心卸载后残留文件。你只需要把镜像拉下来、启动容器,必要时挂载数据卷,服务就能跑起来。这也是为什么容器化部署已经成为当下服务交付的主流方式,Docker则是绝大多数人接触容器化的第一站。这篇文章就围绕这个主题,从安装讲到常见实操,再到Compose编排和排障,希望你看完能自己动手把MySQL、Redis、Nextcloud这类服务跑起来。
1.2 镜像、容器、仓库这三个概念必须拧清
Docker体系里最常被混淆的三个词是镜像、容器和仓库。我习惯用生活里的类比来理解:镜像是一个只读的安装包模板,容器是这个安装包运行起来之后的实例,仓库是存放镜像的远程或本地存储中心。
具体来说,镜像由一层一层的只读文件系统组成,每一条构建指令产生一个层,这些层可以复用。拉取镜像时,如果本地已经有相同层,Docker会直接复用,不会重复下载。容器则是在镜像上增加一个可写层,你可以在里面安装软件、修改配置,但这些修改在容器删除后会丢失,除非你显式提交成新镜像或使用数据卷持久化。仓库的作用则类似应用商店,Docker官方的Docker Hub就是最大的公共仓库,你还可以搭建私有仓库存内部镜像。
我们可以用一个表格来快速区分:
| 概念 | 简单理解 | 生命周期 | 核心命令 |
|---|---|---|---|
| 镜像 | 只读模板 | 可一直存在 | docker build / pull / tag |
| 容器 | 镜像的运行实例 | 可创建、启停、删除 | docker run / start / stop / rm |
| 仓库 | 镜像的存储与分发中心 | 长期存在 | docker push / pull |
把这三个概念分开后,很多命令就不难记了:docker pull是把镜像从仓库拉到本地,docker run是根据镜像创建并启动容器,docker exec是进入运行中的容器执行命令。后面所有实操都是围绕这条链路展开的。
1.3 一个容器生命周期里发生的事
新手容易把 docker run 当成一个简单的启动命令,其实一个完整的容器生命周期包含镜像获取、容器创建、启动、运行、停止、删除等阶段,理解这个流程对排查问题很有帮助。
假设你在命令行执行 docker run -d --name web -p 8080:80 nginx:alpine,Docker会先检查本地有没有 nginx:alpine 镜像,没有就去配置的仓库拉取;拉下来后基于镜像创建一个可写层容器,分配一个容器ID和默认网络端口;然后执行镜像里设置的启动命令,通常是CMD或ENTRYPOINT指定的脚本;如果启动命令在前台运行且没有自动退出,容器就会保持运行状态。当你执行 docker stop web,Docker会先给容器内主进程发送SIGTERM信号,等待超时后再发SIGKILL强制结束;执行 docker rm web 才会删除这个容器,包括它的可写层。
容器里的进程是隔离的,但从系统角度看,它仍然是宿主机上的进程,只是通过命名空间限制了对其他资源的可见性。这也是为什么容器启动很快,因为不需要像虚拟机那样引导整个操作系统。理解了这一层,你再去理解 docker logs、docker restart、docker inspect 这些命令,就会清楚它们分别作用于生命周期的哪个阶段。我自己排查问题时的顺序通常是:docker ps 看容器在不在,docker logs 看输出,docker inspect 看配置,定位到原因再决定是重启、删除重建还是进入容器内修改。
2. 装好Docker:Windows、Linux和macOS的路数不一样
2.1 Windows装Docker Desktop前先搞定WSL2和虚拟化
Windows上安装Docker最主流的方式是Docker Desktop,但很多人安装后第一步就卡住了,最常见的报错是"Docker Desktop failed to start because virtualisation support wasn't detected"。这个报错本质是Windows没有检测到虚拟化能力,或者虚拟化没对齐。
处理步骤依次是:先确认CPU虚拟化是否已在BIOS/UEFI中开启,Intel平台叫Intel VT-x,AMD平台叫AMD-V;再检测Windows的虚拟化相关功能是否启用,包括"虚拟机平台"和"适用于Linux的Windows子系统(WSL)"。在PowerShell管理员模式下执行 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart,然后再执行一条开启虚拟机平台的命令,dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart,完成后重启系统。
Docker Desktop依赖WSL2作为Linux后端。很多情况下,即使虚拟化已开启,WSL2内核没更新或没有安装发行版也会导致启动失败。启动WSL报"wsl --install"是一种方式,但老版本Windows需要手动从微软官网下载WSL2内核更新包并执行 wsl --set-default-version 2。装好WSL2后,打开Docker Desktop的Settings,在Resources->WSL Integration里确认你要用的发行版已经启用。如果还看到"we've detected that you have an incompatible version of windows"这类提示,那就是Windows版本过旧,Docker Desktop新版本要求Win10 21H2或更高版本的系统,升级系统或安装对应旧版Docker Desktop是两条常见出路。
2.2 Linux下安装Docker Engine:别偷懒用在线脚本
在Linux服务器上安装Docker,很多教程会让你直接执行 curl -fsSL https://get.docker.com | bash -s -- 这个在线脚本,确实很快,但我建议你在生产环境里走官方仓库安装,原因有两点:一是网络脚本下载的内容不确定,你无法完全掌控它装了哪个版本;二是企业内部环境往往有代理和yum或apt源规范,直接在线脚本容易绕过这些配置。
以Ubuntu为例,常规安装流程是先更新apt索引,然后安装 ca-certificates curl gnupg lsb-release 这些依赖,之后添加Docker官方GPG密钥和仓库,再 apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin。CentOS 7升级Docker则会涉及移除旧版本、安装yum-utils、配置仓库、安装docker-ce、设置开机自启等步骤。最容易被忽略的一步是给当前用户加docker组,避免每次敲命令都要sudo:执行 sudo usermod -aG docker $USER,然后重新登录当前会话。
Linux安装完成后,建议先验证一下三件事:docker --version 看客户端版本,docker compose version 看Compose插件是否存在,sudo systemctl enable --now docker 设置开机启动。如果你安装的是老版本没有compose插件,也可以单独安装docker-compose二进制文件。Docker服务启动失败时,用 journalctl -u docker 查看日志,比盲目重装高效得多。
2.3 装到D盘这种需求,其实是改WSL的数据目录
很多Windows用户安装Docker Desktop时关心一件事:能不能装到D盘,别把C盘塞满。这里的"装"其实分两部分:安装程序本身可以指定安装目录,但Docker Desktop默认的镜像、容器、卷数据都存放在C盘的用户目录下,更准确的说是放在WSL2虚拟磁盘文件里。Docker Desktop on Windows的背后是一个叫 docker-desktop 的WSL发行版和一个 docker-desktop-data 的数据发行版,这两个虚拟磁盘的vhdx文件通常位于C:\Users<你的用户名>\AppData\Local\Docker\wsl。
想把Docker数据迁移到D盘,一个常见的办法是先在Settings里关闭Docker Desktop,然后使用 wsl --shutdown 停掉WSL,再导出这两个发行版的vhdx文件,再注销并导入到D盘目标路径。具体命令类似 wsl --export docker-desktop D:\docker\docker-desktop.tar,然后 wsl --unregister docker-desktop,再 wsl --import docker-desktop D:\docker\docker-desktop D:\docker\docker-desktop.tar,docker-desktop-data也照做。操作完成后重新启动Docker Desktop,再观察磁盘占用。
需要特别注意的是,这种操作对新手来说有一定风险,如果操作不当可能导致Docker Desktop无法启动。我现在会更推荐一个相对简单的方案:先安装Docker Desktop到D盘,安装完成后把WSL数据目录通过上述方式迁移,或者干脆直接使用Windows下的WSL发行版作为工作环境,在WSL内安装Docker Engine,这样虚拟磁盘文件也可以自定义位置。无论哪种方式,迁移前都先备份重要容器和数据卷,迁移后再用 docker images 和 docker ps -a 验证数据是否完整。这也是"docker desktop installer.exe怎么装到D盘"这类问题背后真正需要做的事。
3. 第一次实操:从运行nginx到操作MySQL容器
3.1 拉镜像慢?先换一个能用的镜像加速源
镜像下载慢几乎是所有国内新手遇到的第一个真实痛点,默认连接Docker Hub速度不稳定,几十MB的镜像可能要几分钟甚至失败。解决思路是为Docker配置镜像加速源,注意这只是加速拉取公共镜像,不影响你后续构建私有镜像。
在Docker Desktop中配置镜像源比较直接:打开Settings->Docker Engine,在JSON里加入"registry-mirrors": ["https://你的加速地址"],保存并重启。在Linux上则是修改 /etc/docker/daemon.json,同样加入registry-mirrors数组,然后执行 sudo systemctl daemon-reload && sudo systemctl restart docker。云厂商一般会为用户提供专属加速地址,阿里云、腾讯云、华为云都有对应文档,你在自己账号的控制台里能找到专属URL,填进去即可。
这里提醒一句:镜像源只影响从仓库拉取镜像的速度,不影响容器运行时的网络。如果你用的是自定义证书仓库或私有仓库,还需要在daemon.json里配置insecure-registries,但公共环境不建议这么干。配置完之后,可以用 docker info 查看Registry Mirrors是不是生效,拉一个大镜像测试,比如 docker pull mysql:8.0,速度如果明显提升就说明配置成功了。
3.2 跑一个前台容器,把端口映射和日志输出看明白
第一次实操我建议先用nginx,因为镜像小、启动快,不容易出幺蛾子。执行 docker run -d --name myweb -p 8080:80 nginx:alpine,这条命令里-d表示后台运行,--name给容器起名字,-p 8080:80把宿主机的8080端口映射到容器内的80端口。启动后访问 http://localhost:8080,看到Nginx欢迎页就说明你的第一个容器已经跑起来了。
但仅仅跑起来还不够,你得学会观察它。 docker ps 看运行中的容器,加-a参数看所有容器; docker logs -f myweb 实时看日志输出; docker stats myweb 看CPU、内存和网络吞吐。端口映射这里最容易踩坑的是端口被占用和短时间内反复重启,当你发现浏览器访问不了时,先看 docker ps -a 里容器的STATUS是不是Up或Exited,Exited说明启动后进程退出,要立刻 docker logs myweb 查原因。
如果想更深入理解容器,可以执行 docker inspect myweb,里面会输出容器的完整元数据,包括网络IP、挂载卷、环境变量、健康检查等,排查问题非常有用。我个人习惯把 docker inspect 当作容器的"身份证+体检报告",很多诡异问题都能在这里找到线索。等nginx跑通后,我建议你执行 docker stop myweb && docker rm myweb 清理掉,再带着 --restart unless-stopped 参数启动,这样机器重启后容器能自动恢复,是生产环境里常用的做法。
3.3 在容器里执行命令:docker exec -it 的完整用法
实操中你经常会需要进入容器内部,比如查看配置文件、手动执行某个数据库命令,这时候就要用 docker exec。最经典的写法是 docker exec -it mycontainer bash,-i表示保持标准输入,-t分配一个伪终端,合起来才能获得一个可以交互的shell。
以MySQL容器为例,假设你运行了 docker run -d --name mysql8 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql:8.0,想在里面执行SQL,可以先 docker exec -it mysql8 bash 进入容器,然后执行 mysql -uroot -p123456 登录。不过并不是所有镜像都自带bash,比如alpine基础镜像只有一个sh,这时你要把命令改成 docker exec -it 容器名 sh。为了避免反复进入容器,也可以直接执行 docker exec -it mysql8 mysql -uroot -p123456,Docker会把命令直接交给容器内进程执行,输出直接显示在宿主机终端上。
还有一个常被忽略的细节:docker exec 和 docker attach 不一样。attach是把当前终端挂到容器的主进程上,一旦容器主进程退出,终端也会跟着退出;exec则是创建一个新的进程并在其中执行命令,不会干扰容器主进程。所以日常调试优先用exec,docker logs 看日志,不到万不得已不要用attach。
4. 数据卷和网络:让容器不可怕的关键两步
4.1 容器一删数据就没了,所以要用volume
有了一次实操经验后,你会遇到一个很现实的问题:容器删除后,里面的数据也跟着没了。比如跑MySQL容器,在容器里创建了数据库和表,执行 docker rm mysql8 清掉容器,再重新run,数据全部消失。这是因为容器可写层和容器生命周期绑定在一起,容器删除后随之销毁。
Docker提供的解决方案是数据卷(volume)。卷本质上是一个宿主机上的目录,被挂载进容器的指定路径,容器读写这个路径时实际读写的是宿主机目录,和容器生命周期解耦。Docker Compose和运行命令里常见的卷类型有三种:命名卷、匿名卷和绑定挂载。
| 类型 | 创建方式 | 适用场景 |
|---|---|---|
| 命名卷 | docker volume create mydata,或 -v mydata:/var/lib/mysql | 持久化数据,推荐 |
| 匿名卷 | -v /var/lib/mysql | 临时数据,容器删除后卷可能残留 |
| 绑定挂载 | -v /host/path:/container/path | 开发环境改代码即时生效 |
MySQL官方镜像在Dockerfile里声明了VOLUME /var/lib/mysql,所以即使你没手动指定卷,docker run时也会创建一个匿名卷来保存数据。但匿名卷很难管理,下次启动时不容易复用,所以我建议你始终显式指定命名卷,比如 docker run -d --name mysql8 -e MYSQL_ROOT_PASSWORD=123456 -v mysql-data:/var/lib/mysql mysql:8.0。这样删掉容器后,卷还在,重新run同一个镜像并挂载同一个卷,数据就能回来。
4.2 自定义网络和容器名解析:Redis主从就是这么连起来的
单容器玩明白后,自然会遇到多个容器需要互相通信的场景。比如Redis主从架构,主节点和从节点都在Docker里跑,从节点需要连到主节点。很多人这时候会直接用IP地址连接,但容器重启后IP会变,配置就不稳定。
Docker里的更优雅做法是创建自定义网络,然后通过容器名互相访问。执行 docker network create redis-net 创建一个bridge网络,然后分别用 --network redis-net 启动两个Redis容器。只要在同一个自定义网络里,容器之间就能通过容器名解析到对方IP,这是内置DNS服务提供的能力。启动主节点时加 --name redis-master,启动从节点时加 --name redis-slave,从节点内部执行 redis-cli replicaof redis-master 6379,Redis会自动把 redis-master 解析成主节点容器的IP。
这个特性不仅对Redis好用,对任何需要服务发现的架构都通用。默认bridge网络虽然也有DNS,但它需要在启动时用--link参数做链接,已经是老方法了,自定义网络要干净得多。所以我现在遇到多个容器需要协作的场景,第一反应是先 docker network create,而不是手动查IP写死配置。你可以把自定义网络理解成容器之间的专用局域网,容器名就是局域网里的主机名,这样整个方案就非常好理解。
4.3 MySQL数据持久化与密码配置的实操细节
前面提到MySQL容器,这里展开说说参数细节。正常运行一个MySQL 8.0容器,我通常会这样写:
docker run -d --name mysql8
-p 3306:3306
-e MYSQL_ROOT_PASSWORD=StrongPass123
-e TZ=Asia/Shanghai
-v mysql-data:/var/lib/mysql
-v $(pwd)/conf:/etc/mysql/conf.d
--restart unless-stopped
mysql:8.0
这里关键点有四个。第一,MYSQL_ROOT_PASSWORD是初始化数据库时设置root密码的环境变量,只在数据目录为空、首次初始化时生效,如果卷里已有数据,改环境变量不会改密码。第二,TZ=Asia/Shanghai是时区设置,虽然容器底层默认是UTC,但很多业务日志和函数依赖本地时间,建议一开始就配好。第三,数据卷挂到/var/lib/mysql,日志打印到stdout,你可以用docker logs看MySQL的启动日志。第四,第二个-v是把宿主机当前目录下的conf目录挂到容器内的MySQL配置扩展目录,想调字符集、慢查询日志等参数,直接改宿主机配置文件再重启容器即可。
还有一个容易忽略的点:宿主机3306端口如果被其他MySQL占用了,容器启动会失败。你可以换一个端口映射,比如-p 3307:3306,这样容器内MySQL还是监听3306,宿主机用3307访问。连接信息里的端口要注意区分,外部工具连的是宿主机的3307,容器内部进程互访用3306,初学者经常在这里踩坑。
5. 多容器编排:用Docker Compose把Nextcloud和Kodbox跑起来
5.1 Compose文件的基本骨骼:services、images、volumes、networks
手动用docker run启动多个容器,虽然在理解原理上很有帮助,但一旦服务多起来,命令会非常长,而且容易漏参数。Docker Compose的价值就是把多容器的启动配置写在一个YAML文件里,通过 docker compose up -d 一次性拉起所有服务。
Compose文件的核心结构是services、volumes、networks三层。services下定义每个服务,对应一个容器;volumes声明命名卷;networks声明自定义网络。一个最基本的Compose例子如下:
services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: myapp volumes: - db-data:/var/lib/mysql networks: - app-net app: image: nginx:alpine ports: - "8080:80" networks: - app-net volumes: db-data: networks: app-net:这个文件看起来不长,但信息密度很高。image指定镜像,environment传环境变量,volumes挂载卷,ports映射端口,networks绑定网络。Compose会自动为同一个项目创建独立网络,所有服务默认可以互相使用服务名访问,所以你不需要手动创建网络。开发环境里还可以用 version字段,但新版Compose已经不太要求显式指定版本了。
刚开始写Compose时,我建议先把单个服务跑通,再加另一个,每次修改后执行 docker compose config 校验文件格式没问题,再 docker compose up -d 应用。不要一次性堆一大堆服务,否则某个服务启动失败时,排查成本会变大。实践下来,Compose文件本身也是一种文档,它能清楚表达服务之间的依赖关系,比口头说明"这个服务怎么启动"靠谱得多。
5.2 Nextcloud自带Apache,为什么还要自己写端口和目录
热词里有一条是"容器化部署nextcloud自带apache",很多人困惑:Nextcloud官方镜像是不是自带Web服务器?答案是自带的。官方镜像基于Apache和PHP,镜像内已经装好了Apache,你不需要额外跑一个nginx或httpd容器。你只要把镜像的80端口映射到宿主机,并挂载数据目录,就能访问Nextcloud的安装页。
一个能用的Compose片段可以这样写:
services: nextcloud: image: nextcloud:apache ports: - "8081:80" volumes: - nextcloud-data:/var/www/html environment: - NEXTCLOUD_ADMIN_USER=admin - NEXTCLOUD_ADMIN_PASSWORD=adminpass - MYSQL_HOST=db - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud - MYSQL_PASSWORD=secret depends_on: - db db: image: mariadb:10.6 volumes: - db-data:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD=rootpass - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud - MYSQL_PASSWORD=secret volumes: nextcloud-data: db-data:注意这里我把Nextcloud的Web端口映射成了8081,避免和本机80端口冲突。NEXTCLOUD_ADMIN_USER和NEXTCLOUD_ADMIN_PASSWORD是首次安装时自动创建管理员用的环境变量。Nextcloud不接数据库时默认会用SQLite,但生产环境一般选MySQL或MariaDB,所以在环境变量里指定了数据库连接信息。depends_on表示Nextcloud要在数据库服务启动后再启动,但注意它只控制顺序,不保证数据库已经可用,必要时可以加健康检查。
数据目录挂在/var/www/html下,这个目录是Nextcloud应用代码和数据默认的根目录。挂载后,应用文件、用户上传文件都会持久化在宿主机卷里。Nextcloud在后端需要定期执行cron任务,这个在Compose里可以额外加一个cron容器,但作为入门先不展开。能通过浏览器打开安装页并创建管理员,就算是把这个经典网盘应用跑通了。