在本地把容器跑通不算完,真正让项目“能用”,是把这一整套东西搬到云服务器上、让别人也能正常访问的那一刻。我自己第一次做Docker部署时,想得特别简单——本地都跑起来了,传上去不就行了?结果在服务器上折腾了大半个晚上,端口不通、容器秒退、时区差8小时,各种问题扎堆冒出来。这篇内容就是把我后来总结出来的一整套部署流程完整走一遍,从服务器初始化到镜像构建、Compose编排、稳定性设置,再到当时的排错思路,希望能让你一次少踩几个坑。
1. 从一台刚开通的云主机开始:初始化系统与Docker Engine安装
1.1 服务器选型和系统划分,别一上来就装桌面环境
选云服务器这件事,很多教程直接跳过,但它决定了后面部署的顺畅程度。如果你只是跑Docker项目,2核4G起步就够用,跑MySQL、Redis再加两三个业务容器,内存会比较紧张,但也不是不能活。预算允许的话,4核8G会舒服很多,至少不用整天盯着内存报警。
另一个关键点是操作系统。我用的是Ubuntu 22.04 LTS,不是因为它比CentOS高档,而是因为Docker官方源和社区资料都更偏向Debian系,安装时遇到的坑最少。系统选好后进入服务器,第一件事不是装Docker,而是把系统做一次基本体检:
bash复制sudo apt update && sudo apt upgrade -y
sudo timedatectl set-timezone Asia/Shanghai
时区这个问题我吃过一次亏。默认情况下很多云主机的系统时间是UTC,你部署完应用发现日志时间比北京时间慢8小时,第一个反应是查代码,查了半天才发现是系统时区的问题。timedatectl 这一步建议在装任何东西之前就做掉。
还有一点想认真建议:生产环境不要装宝塔面板。不是说面板不能用,而是面板为了兼容性往往自带一套软件管理逻辑,它和Docker的端口映射、防火墙规则有时候会互相打架。你排查一个端口不通的问题,结果发现是面板的防火墙和系统防火墙两层都在拦,这种经历一次就能让你血压拉满。既然要用Docker,就尽量让系统保持干净,少用额外的一键工具。
1.2 通过官方源安装Docker Engine,而不是复制粘贴脚本
网上一搜Docker安装教程,满屏都是“
bash复制curl -fsSL https://get.docker.com | bash -s docker
”这种一键脚本。这方法确实快,但我不推荐在正式环境用。原因很简单:脚本跑完你并不知道它改了哪些系统配置,更麻烦的是,以后想升级、想卸载、想排查问题的时候,缺乏一个清晰的包管理记录。用官方apt源安装,整个过程都在包管理器掌控之下,逻辑更干净。
我的习惯是把这几条命令完整走一遍:
bash复制sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
装完之后,顺手启动并设置开机自启:
bash复制sudo systemctl enable docker --now
注意最后一条命令里我直接带了--now参数,省去先start再enable两步操作。如果你看到docker: Cannot connect to the Docker daemon这类报错,绝大多数是因为Docker服务没起来,先执行systemctl status docker确认服务状态,再继续排查。
1.3 初始化配置与验证:权限、内核模块和网络
Docker装完后,我不急着跑容器,先做三件事:
第一,把当前用户加入docker组,这样不用每次敲sudo docker。加完用户组需要重新登录会话才生效:
bash复制sudo usermod -aG docker $USER
第二,确认overlay2存储驱动在工作。Docker默认会启用overlay2,但有些云主机的内核模块没有自动加载,导致回退到vfs,那磁盘占用会大得离谱。用docker info看一眼Storage Driver字段就够了:
bash复制docker info | grep "Storage Driver"
如果显示的是overlay2,一切正常;如果是vfs,需要检查内核模块modprobe overlay并考虑重启系统。
第三,配置国内镜像加速。这一步国内服务器基本绕不过去,不然拉取公共镜像会慢到怀疑人生。编辑/etc/docker/daemon.json,写入:
json复制{
"registry-mirrors": ["https://docker.m.daocloud.io"]
}
修改后重启Docker:
bash复制sudo systemctl restart docker
镜像加速器是提高部署体验最实在的一步,不配置的话,一个几百MB的基础镜像能拉上十几分钟,配好之后基本是几秒到几十秒的事。
到这里,服务器侧的环境就算准备好了。按我的经验,整个初始化过程控制在15分钟以内是比较理想的,不要在上面耗太多精力,后面才是更有意思的部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把本地草稿变成标准镜像:Dockerfile、构建与镜像仓库推送
2.1 写好Dockerfile和.dockerignore,从源头避开“镜像有毒”的坑
本地项目能跑起来,到Docker镜像能正常启动,中间隔着的不止一个Dockerfile。很多第一次做镜像的人直接把整个项目目录COPY进容器,结果node_modules、target、.git这些动辄几百MB的东西全被打进镜像,构建慢、体积大、还有各种权限乱象。
一个合理的Dockerfile至少要考虑三点:基础镜像选择、构建上下文、运行时依赖。
以我最近部署的一个Java Spring Boot项目为例,Dockerfile长这样:
dockerfile复制FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
这个Dockerfile用了多阶段构建:第一阶段用Maven镜像编译打包,第二阶段只保留JRE和最终的jar包,镜像体积能小一半还多。ENV TZ=Asia/Shanghai 这一行顺手把时区问题也解决了。
与Dockerfile同样重要的还有.dockerignore:
dockerfile复制.git
target/
*.iml
.idea/
node_modules/
别小看这个文件,没有它,docker build时会把整个项目目录作为上下文发送给Docker守护进程。项目里有个几百MB的日志目录或者依赖目录,构建过程直接慢到怀疑人生。
Python项目我习惯用python:3.11-slim而不是python:3.11,slim版本砍掉了大量用不到的系统工具,镜像体积能少将近一半,攻击面也更小。Node项目同理,node:20-alpine比node:20更适合做运行时镜像。
2.2 构建参数、平台差异和镜像标签策略
构建镜像时有一个非常容易踩的坑:平台不一致。如果你在MacBook上构建镜像,默认架构是arm64,而云服务器大概率是x86_64,直接把本地镜像传上去跑会发现启动就报exec format error。
解决这个问题的思路有两个。简单粗暴的方式是在服务器上直接构建,保证架构一致,适合小项目。正式一点的方式是用Docker Buildx做跨平台构建:
bash复制docker buildx build --platform linux/amd64 -t your-registry.com/myapp:v1.0 . --push
我因为长期在Apple Silicon的笔记本上开发,部署到x86的云服务器,所以Buildx这个命令基本是固定套餐。第一次用的时候需要先创建builder实例:
bash复制docker buildx create --name mybuilder --use
而镜像标签这件事,很多教程轻描淡写,实际却直接影响生产安全。我的原则很简单:绝对不用latest做生产镜像。你永远不知道latest对应的是哪个版本,一旦服务器在某个周末拉了一个内容已经变化的latest,应用就可能在不明不白中更新了。
我的标签方案是:镜像名:版本号-时间戳,比如myapp:v1.0-20250601。构建时用git tag或者CI里的版本号来控制镜像标签,这样每次部署都能清楚知道线上是哪个版本,回滚时也知道该pull哪个镜像。
2.3 推送到镜像仓库,而不是拿tar包满世界拷
部署Docker项目时,有一类经典操作是把本地镜像用docker save打成tar包,再scp到服务器上docker load。小项目测试用可以,但稍微正规一点就会发现它特别别扭:文件大、传输慢、没有版本管理、服务器上到底加载的是哪个镜像全凭口头确认。
更好的做法是把镜像推送到一个镜像仓库。公网可以用Docker Hub,国内更推荐阿里云容器镜像服务或者自己搭的Harbor。以阿里云ACR为例,登录和推送的命令是:
bash复制docker login --username=你的账号 registry.cn-hangzhou.aliyuncs.com
docker tag myapp:v1.0 registry.cn-hangzhou.aliyuncs.com/your-namespace/myapp:v1.0
docker push registry.cn-hangzhou.aliyuncs.com/your-namespace/myapp:v1.0
推到私有仓库的好处在于,服务器上只需要一行docker pull就能把镜像拉下来,而且所有服务器拉取的都是同一个存证归档版本的镜像,不会出现“我这边的镜像跟那边的不一样”这种问题。
如果你怕隐私泄露,完全可以搭一个私有的镜像仓库,docker run -d -p 5000:5000 --name registry registry:2一行命令跑起来,虽然不带UI,但基本够用。项目多了之后再考虑加认证和TLS。
3. 多容器项目不再需要手工敲一串docker run:Compose编排配置详解
3.1 为什么部署环节要上Compose,而不是一行行docker run
到了服务器上,发现项目不止一个容器:应用一个、MySQL一个、Redis一个,可能还要加一个Nginx做反向代理。如果每个容器都用docker run敲命令,参数加起来几十行,一次两次能忍,到了第三次部署必定漏参数。而且服务的启动顺序、网络互通、数据卷挂载这些凭记忆维护相当不现实。
Docker Compose解决的就是这个问题。用YAML文件把一组容器的运行方式描述清楚,一个docker compose up -d全部搞定。我现在对待Docker Compose的态度很简单:只要是多个容器的项目,一律上Compose,哪怕是单容器的项目,也会用Compose管理,因为配置文件本身就是一份可读的部署文档。
还要特别提醒一点,Docker Compose现在已经是Docker官方的插件,不需要单独装什么docker-compose老版本软件,直接docker compose(中间有空格)就是标准用法。
3.2 一份能直接复用的docker-compose.yml长什么样
我写Compose文件一般会这样组织。以“应用 + MySQL + Redis”的经典组合为例:
yaml复制services:
app:
image: registry.cn-hangzhou.aliyuncs.com/your-namespace/myapp:v1.0
container_name: myapp
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
TZ: Asia/Shanghai
DB_HOST: mysql
DB_PORT: 3306
DB_USER: appuser
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
ports:
- "8080:8080"
networks:
- app-network
mysql:
image: mysql:8.0
container_name: myapp-mysql
restart: unless-stopped
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
- ./init-sql:/docker-entrypoint-initdb.d:ro
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
volumes:
mysql-data:
redis-data:
networks:
app-network:
driver: bridge
这份配置里有几个细节值得琢磨。
第一是depends_on配合healthcheck。光写depends_on只能保证容器启动顺序,不能保证MySQL真正就绪。应用容器可能在MySQL还没完成初始化时就尝试连接导致报错崩溃。加了condition: service_healthy之后,Compose会等MySQL健康检查通过后才启动应用,这个机制非常必要。
第二是container_name。显式指定容器名称,部署后一眼就能看出哪些容器属于哪个项目,docker logs myapp也方便。如果不指定,容器名会带项目名前缀,比如myproject_app_1,日志和排查时要多记一层映射关系。
第三是数据卷。MySQL和Redis的数据都在volumes里声明并挂载到具名卷,容器删了重建数据还在。这是数据库容器绝不可省略的一步。如果不用具名卷,容器被docker compose down后数据直接消失,到时候哭都来不及。
3.3 环境变量和敏感信息的管理思路
Compose文件里我用了${DB_PASSWORD}这样的占位符,这些变量从哪来?一般放在同目录下的.env文件里:
bash复制MYSQL_ROOT_PASSWORD=R00tP@ssw0rd!ChangeMe
DB_PASSWORD=AppPassw0rd!ChangeMe
REDIS_PASSWORD=RedisPassw0rd!ChangeMe
.env文件的便利之处是它会被Compose自动读取,不用export也别写在代码里。但这里有个安全细节:.env文件包含明文密码,而且它不随Docker镜像走,只存在于服务器上。所以你绝不应该把这个文件提交到Git仓库。给你的.env.example版本加上明确的说明,生产服务器的.env权限尽量收窄:
bash复制chmod 600 .env
还有一个更谨慎的做法,是用Docker Secret或者云厂商的密钥管理服务。不过单独说,对大多数中小项目来说,.env加权限控制已经够用,只要别手滑把它提交到公开仓库就好。我见过不止一次有人在GitHub上把.env传到公开库,然后半夜收到云厂商的密码爆破警报。
4. 从仓库到服务器:拉取镜像、端口放开和服务启动验证
4.1 安全组和防火墙的端口放行逻辑,这个是新手重灾区
镜像推到仓库之后,真正的部署动作看起来只有两步:“拉到服务器上”和“启动”,但新手卡得最久的地方反而不是这俩,而是服务器根本访问不到。
云服务器有两层网络过滤:云控制台里的安全组和系统防火墙。很多人改了安全组忘了清系统防火墙,或者只打开了系统防火墙却在安全组里没加端口,最终效果都是一样的——外部访问不了。
以阿里云为例,安全组的规则需要在控制台操作,把8080、3306、6379等端口加进安全组入方向,源地址建议按需限制而不是设置0.0.0.0/0。系统防火墙这边,如果你用的Ubuntu默认启用了ufw:
bash复制sudo ufw allow 8080/tcp
sudo ufw allow 3306/tcp
sudo ufw allow 6379/tcp
sudo ufw reload
这里想多提醒一句:3306和6379如果不需要公网访问,就不要在安全组里放行,只允许内网访问就够了。明白人应该懂我的意思——你部署的MySQL对公网开放,等于把自己的数据裸露在C段扫描器面前。我的习惯是这种端口只让服务器内网访问,应用容器通过Compose网络直接连,安全组完全不放行。
4.2 首次拉取与启动的命令链路
在服务器上创建一个项目目录,比如/opt/myapp,把docker-compose.yml和.env传上去,然后开始部署:
bash复制cd /opt/myapp
docker compose pull
docker compose up -d
docker compose pull把所有镜像拉到本地,up -d在后台启动所有容器。第一次跑的时候建议先别加-d,直接docker compose up让日志刷在前面,确认所有容器都能正常启动后再按Ctrl+C停掉,改用-d正式启动。
这里有个权衡:生产环境我通常还是会直接up -d,然后用下一节讲的命令去看状态和日志。如果你能接受前台启动并仔细看一遍启动日志,那第一次用前台模式会更直观。
4.3 日志、Healthcheck与启动失败的快速判断
容器启动“成功”不等于“服务正常”。接下来几步是我固定的验证动作:
bash复制docker compose ps
这条命令能列出所有容器的状态。如果某个容器反复显示Restarting,基本可以判断它在崩溃重启。这时候看日志:
bash复制docker compose logs --tail=200 app
日志里会直接告诉你答案——是数据库连不上、端口被占、还是配置项缺失。如果日志里出现:
text复制Caused by: java.sql.SQLException: Access denied for user 'appuser'@'...'
那就是数据库账号密码不匹配,去.env和数据库实际用户里查。如果你登录MySQL容器看:
bash复制docker exec -it myapp-mysql mysql -uroot -p
进去后发现用户host限制是localhost,而应用连接来源是Compose网络的容器IP,也会报错。需要在MySQL里给appuser授权时使用'appuser'@'%'而不是'appuser'@'localhost',这个坑我在章节里特意标注出来,因为很多MySQL初始化脚本写的都是local。
还有一种情况是容器状态是Up但服务不可用,这时可以看下Healthcheck的状态:
bash复制docker inspect --format='{{json .State.Health}}' myapp
如果返回里没有"healthy",而是"unhealthy",说明进程活着但业务已经不响应了。这种情况优先看应用日志,大概率是依赖的外部服务超时或者连接池耗尽。
5. 上线只是开始:重启策略、资源限制、日志轮转与数据备份
5.1 容器真的挂了能自己拉起来吗
云服务器重启、Docker服务重启、容器进程意外退出,这些情况在你的项目上线后一定会遇到。如果没有配置重启策略,容器挂了就挂了,你得自己发现然后手工拉起来。如果配置得当,Docker会在容器退出之后自动重启,把服务恢复的时间缩短到几秒。
在Compose里,我给每个服务都写了restart: unless-stopped,这是目前最稳妥的策略。它的含义是:Docker守护进程启动时自动拉起容器,除非你手动执行过docker compose stop。也就是说,服务器重启后容器自动回来,而你主动停掉的容器不会被自动拉起,这对维护窗口很友好。
反过来,restart: always虽然也能自动重启,但它没有“用户手动停止后不再启动”的逻辑,有时候你只是想临时停个容器,服务器一重启它又自己起来了,增加了误操作的迷惑性。所以我更推荐unless-stopped。
5.2 内存和CPU不设限,迟早把服务器搞垮
没限制的容器就像没上保险的炸弹。一个Java应用容器可能吃掉2G内存,再加上MySQL和Redis,如果应用出现内存泄漏或者流量突然暴涨,整台云服务器会OOM,不只是某个容器挂掉,而是连SSH都连不上。我经历过一次,浏览器在终端里等了半分钟才反应,那时候只能去云控制台强制重启。
所以内存限制必须提前设好。Compose里用deploy.resources.limits:
yaml复制services:
app:
deploy:
resources:
limits:
memory: 1.5G
cpus: '1.0'
注意,deploy节点在docker compose(Docker Compose v2)里是可以直接生效的,但旧版docker-compose会忽略这个字段。如果你系统里装的是老版本,建议升级到Compose v2。
Redis这种纯内存缓存更得加限制。不加的话,一旦数据量大起来,它可能会把宿主机内存吃光。我一般会给Redis设maxmemory 512mb,启动命令写成:
yaml复制command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass ${REDIS_PASSWORD}
allkeys-lru表示内存满时优先淘汰不常用的键,适合缓存场景。
还有一个容易忽略的参数是日志轮转。Docker默认会无限收集容器的stdout日志,一个写日志很凶的容器,几天就能产生几个G的日志文件,把磁盘占满。在daemon.json里加上全局日志限制:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
设置完之后重启Docker服务,新容器就会遵从这个规则,每个容器日志最多30MB,超过自动滚动清理。
5.3 数据卷备份与恢复:不管什么时候都要留后路
容器可以随时重建,但数据库数据没有备份就是灾难。MySQL数据在具名卷mysql-data里,备份思路是直接用mysqldump导出逻辑数据:
bash复制docker exec myapp-mysql mysqldump -uroot -p"密码" --databases myapp > myapp-$(date +%F).sql
备份脚本挂到cron里每天执行一次:
bash复制0 3 * * * cd /opt/myapp && docker exec myapp-mysql mysqldump -uroot -p"密码" --databases myapp > backups/myapp-$(date +\%F).sql
这里有个小陷阱:在crontab里写$(date +%F),百分号需要用反斜杠转义。我第一次写cron任务时没注意,脚本跑了好几天,文件名全部是空括号,差点以为备份没生效。
恢复的方式是把SQL文件导入到容器内:
bash复制docker exec -i myapp-mysql mysql -uroot -p"密码" < myapp-2025-06-01.sql
Redis的持久化数据在redis-data卷里,恢复相对简单,直接把卷里的appendonly.aof或dump.rdb替换回去就行。但更常用的Redis恢复方式是让应用重新从数据库加载缓存,所以我一般不会太纠结Redis备份,MySQL才是底线。
6. 部署现场的五连坑:OOM、挂载权限、时区、端口冲突和进度卡死
6.1 容器启动后秒退:OOM与资源限制的排查链
遇到过最经典的“灵异事件”是:容器状态一下Up一下Restarting,日志却只显示一两行就断了。第一反应是代码有问题,但仔细看日志完全没有报错堆栈,只有JVM或者主进程直接被终止的痕迹。
这套现象的无凶之一是内存不足被操作系统杀掉。云服务器2G内存,MySQL就占了800M,再启动一个Java应用,内存立刻破1.5G,系统触发OOM killer,把进程杀掉。这种时候docker logs往往是空的,因为应用没有机会打出任何日志。
排查链路是这样的:
bash复制dmesg | grep -i "oom"
journalctl -k --since "10 minutes ago" | grep -i "oom"
如果看到类似Out of memory: Kill process的记录,就是OOM无误了。解决方向不是光加内存条,而是合理分配容器内存。MySQL、Java、Redis每个容器都设好内存上限,宁可让某个容器启动失败也不要让整个服务器挂掉。
6.2 挂载目录没有写权限:从701权限到用户的来龙去脉
挂载宿主机目录到容器里时,经常遇到应用写不进目录的报错。比如Nginx容器挂载了./html目录,宿主机返回Permission denied。为什么?因为在Docker里,运行在容器里的进程是以容器内用户身份去写挂载目录的,宿主机目录的属主和权限直接决定了能不能写。
我的解决方案很直接:创建宿主机目录时就把属主改成容器内进程的UID。比如MySQL容器内进程是mysql用户,UID是999;很多Java应用是root;如果你不确定,进容器里看一眼:
bash复制docker exec -it myapp id
然后宿主机上执行:
bash复制sudo chown -R 1000:1000 /opt/myapp/storage
这里1000通常是应用容器内普通用户的UID。如果找不到合适UID,最简单粗暴但有效的方式是让目录对所有人可写:
bash复制sudo chmod -R 777 /opt/myapp/storage
但637权限意味着任何用户都能改动这些文件,在生产环境不太推荐。对一个目录临时用一下可以,正确做法还是按应用用户来设定属主。
6.3 MySQL时区导致的时间差8小时问题
时区问题在Docker化之后变得更隐蔽。宿主机设置为Asia/Shanghai,容器里默认还是UTC,MySQL存的时间戳就按UTC走。应用读出来的时间可能正确也可能不正确,取决于JDBC连接串里有没写serverTimezone=Asia/Shanghai。
我现在的方案是三个地方同时设时区,缺一不可:
首先在MySQL容器里:
yaml复制environment:
- TZ=Asia/Shanghai
- MYSQL_INITDB_ARGS="--default-time-zone=+08:00"
其次在应用连接串里加参数:
text复制jdbc:mysql://mysql:3306/myapp?serverTimezone=Asia/Shanghai&useSSL=false
最后是Dockerfile或Compose环境变量里设TZ=Asia/Shanghai。
三个地方一致之后,MySQL的NOW()函数、应用的当前时间、日志时间戳就都不会再出现8小时错位。
6.4 端口冲突:从5000到3306的bind错误
端口冲突是高频出现的低级错误,但排查起来也不算简单。最常见的提示是:
text复制Error response from daemon: driver failed programming external connectivity on endpoint
Bind for 0.0.0.0:3306 failed: port is already allocated
看到port is already allocated,说明宿主机上已经有进程占用了3306端口。排查命令:
bash复制ss -lntp | grep 3306
如果显示被某个进程占用,而且占用者不是一个简单进程,那八成是你之前启动过一个MySQL容器没清干净。解决方式:
bash复制docker ps -a | grep mysql
docker rm -f <container_id>
还有一种情况是宿主机本身装了MySQL,它和容器内MySQL都想占3306。同一个端口别让两个角色来抢,要么停掉宿主机MySQL,要么改容器的端口映射,比如"3307:3306"。我个人倾向改映射而不是停宿主机服务,至少不用动机器上其他依赖。
6.5 构建和推送过程中卡住的常见原因
构建镜像时进度停在某一步很久不动,很多人第一反应是网络问题。对,多半真是网络问题,但具体是哪种网络问题需要分情况:
拉取基础镜像慢,那是镜像源问题,按第1节配好镜像加速器后基本解决。
推送镜像到仓库慢,更多是公网带宽瓶颈。如果你的镜像有几个百MB,推送慢是正常的,这不是网络故障。一些云厂商的容器镜像服务在跨地域访问时特别慢,比如部署在华北的服务器去华东的镜像仓库拉镜像,延时明显上升。解决思路是尽量选择与服务器同区域的镜像仓库,或者把镜像推送到离服务器最近的仓库节点。
还有一个常见的卡住场景是docker build执行到某个RUN指令时像卡死一样。这种情况经常是RUN指令里执行了需要交互的命令,比如apt-get install等用户确认。解决方式是在Dockerfile里提前加上DEBIAN_FRONTEND=noninteractive,或者用RUN apt-get install -y中的-y参数避免交互。
部署这事儿,每次环境都不一样,工具版本也一直在更新,但底层的思路是不变的:环境初始化要干净,镜像构建要可复现,编排配置要能读懂,数据存储要有兜底。我自己现在部署一个新项目时,基本就是按这篇文章的流程走一遍,从服务器初始化到Compose上线,中间踩过的坑越来越少。如果你也打算把自己的Docker项目搬到云服务器上,希望这套流程能帮你把前期最磨人的那段路给走顺了。
