服务器买回来第一件事不是装应用,而是把地基打好 。
本文按实际顺序,完整走一遍一台 4 核 8G 的 Ubuntu 24.04 服务器初始化流程:从磁盘分区、Swap 配置,到 Docker 安装、多用户创建与 sudo 授权,最后用 docker-compose 部署 Nginx 并配置 HTTPS 通配符证书。
本文所有命令均以 root 或 sudo 权限执行,文中域名统一使用占位符,替换成你自己的即可。
一、基础信息确认 先明确本文的环境参数,后续步骤都基于此:
项目
配置
操作系统
Ubuntu 24.04 LTS
CPU / 内存
4 核 8G
Swap
8G(文件方式)
数据盘
500G,分区后挂载到 /opt
Docker
apt 仓库自带 docker.io
容器编排
docker compose(插件形式)
SSH 端口
2342
时区
Asia/Shanghai
查看当前环境:
1 2 3 4 5 6 7 8 9 cat /etc/os-release | grep PRETTY_NAMEnproc free -h lsblk -f
二、配置 8G Swap 物理内存 8G 对于跑 Docker 的服务器偏紧,开 8G Swap 可以缓解内存不足导致进程被 OOM Killer 杀掉的风险。
1. 检查现有 Swap
如果已有 swap 且大小合适,可跳过本节。
2. 创建 Swap 文件 1 2 3 4 5 fallocate -l 8G /swapfile
3. 设置权限并格式化 1 2 3 4 5 chmod 600 /swapfilemkswap /swapfile
4. 启用并设置开机自动挂载 1 2 3 4 5 6 swapon /swapfile swapon --show free -h
写入 /etc/fstab 实现开机自动挂载:
1 2 3 4 5 6 7 8 cp /etc/fstab /etc/fstab.bakecho '/swapfile none swap sw 0 0' >> /etc/fstabmount -a
5. 调整 Swap 使用策略 默认 vm.swappiness=60,意味着内存用到 60% 就开始换页。服务器上通常希望优先用物理内存,内存不足了再 swap ,所以调低:
1 2 3 4 5 6 7 8 9 cat > /etc/sysctl.d/99-swap.conf << 'EOF' vm.swappiness=10 vm.vfs_cache_pressure=50 EOF sysctl -p /etc/sysctl.d/99-swap.conf sysctl vm.swappiness
参数说明
vm.swappiness=10:降低换页倾向。设成 0 会完全禁用 swap,内存紧张时进程直接被 OOM Kill,反而更危险。
vm.vfs_cache_pressure=50:默认 100 表示缓存回收积极。调低可让 inode/dentry 缓存保留更久,减少频繁的磁盘 IO。
三、500G 硬盘分区与挂载 1. 查看磁盘设备名
输出示例:
1 2 3 4 5 NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS vda 252:0 0 50G 0 disk ├─vda1 252:1 0 50G 0 part / └─vda2 252:2 0 498G 0 part vdb 253:0 0 500G 0 disk
vdb 就是新增的 500G 数据盘。注意:实际设备名可能是 /dev/vdb、/dev/sdb 或 /dev/nvme1n1,务必按自己的实际情况确认,误格式化系统盘是灾难性操作。
2. 用 fdisk 创建分区
进入交互界面后的操作序列:
1 2 3 4 5 6 7 8 9 10 11 Command (m for help): n # 新建分区 Partition number (1-4, default 1): 1 First sector (2048-1048575999, default 2048): # 直接回车,用默认值 Last sector, +size or +size% or +sizeT: +1G # 划 1G 给 /boot Command (m for help): n # 再新建一个分区 Partition number (1-4, default 2): 2 First sector (2048-1048575999, default 2048): # 直接回车 Last sector, +size or +size% or +sizeT: +499G # 剩下全给数据分区 Command (m for help): w # 写入分区表并退出
fdisk 交互要点 :看到 (default X) 直接回车接受默认值即可;分区类型保持默认的 Linux 类型;最后必须敲 w 才会生效,敲 q 是放弃。
查看分区结果:
3. 格式化分区 1 2 3 4 5 mkfs.ext4 /dev/vdb1 mkfs.ext4 /dev/vdb2
4. 挂载数据分区到 /opt 1 2 3 4 5 6 7 8 mkdir -p /optmount /dev/vdb2 /opt df -h /opt
5. 开机自动挂载 先获取分区的 UUID:
输出类似:
1 /dev/vdb2: UUID="a1b2c3d4-e5f6-7890-abcd-ef1234567890" TYPE="ext4"
写入 /etc/fstab:
1 2 3 4 5 6 7 8 9 10 cp /etc/fstab /etc/fstab.bakecho 'UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 /opt ext4 defaults,noatime 0 2' >> /etc/fstabmount -a df -h /opt
为什么要用 UUID :设备名(/dev/vdb)在重启或热插拔后可能变化,UUID 是分区唯一标识,不会变。用 UUID 挂载是生产环境的标准做法。
6. 确认 /boot 分区用途 上面的 /boot 分区是独立划出的,用于存放内核引导文件。查看当前系统的 /boot 使用情况:
如果系统根分区 / 所在磁盘还很大(且根分区本身就是独立分区),可以不单独划 /boot ——把所有空间给 /opt 即可。保留上面这个划分是为了结构清晰、便于后续重装系统时保留数据。
四、安装 Docker 1. 卸载可能存在的旧版本 1 2 3 for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do apt-get remove -y $pkg done
2. 安装 Docker Ubuntu 24.04 官方仓库自带 docker.io,直接装即可:
1 2 apt-get update apt-get install -y docker.io docker-compose-v2
验证:
1 2 docker --version docker compose version
关于 docker-compose-v2 :这是 Docker Compose V2 的独立包(插件形式),命令是 docker compose(中间有空格),不是旧版的 docker-compose。Ubuntu 24.04 仓库已同时提供两种,建议统一用 V2。
3. 配置日志轮转,防止磁盘被撑爆 Docker 默认不限制日志大小,跑久了 /var/lib/docker/containers/ 会涨到几十上百 G。生产环境必配 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 mkdir -p /etc/dockercat > /etc/docker/daemon.json << 'EOF' { "log-driver" : "json-file" , "log-opts" : { "max-size" : "100m" , "max-file" : "3" }, "storage-driver" : "overlay2" } EOF systemctl restart docker systemctl enable docker docker info | grep -A3 "Logging Driver"
单个容器日志上限 100M × 3 个文件 = 300M。容器多的话可以调大 max-size,但务必设置上限 ,别让日志变成磁盘杀手。
4. 国内镜像加速(可选但强烈建议) 服务器在国内拉取 Docker Hub 镜像会很慢甚至超时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cat > /etc/docker/daemon.json << 'EOF' { "log-driver" : "json-file" , "log-opts" : { "max-size" : "100m" , "max-file" : "3" }, "storage-driver" : "overlay2" , "registry-mirrors" : [ "https://docker.m.daocloud.io" , "https://dockerproxy.com" , "https://docker.1panel.live" ] } EOF systemctl restart docker docker info | grep -A2 "Registry Mirrors"
镜像加速站的可用性会变化,上面几个是目前较稳定的。如果失效了,去阿里云容器镜像服务控制台申请专属加速地址(每账号一个,更可靠)。
5. 普通用户免 sudo 使用 Docker(可选) 把用户加入 docker 组后,该用户就能直接执行 docker 命令,无需 sudo:
1 usermod -aG docker zhangsan
安全提醒 :docker 组等同于 root 权限——用户可以通过挂载宿主机根目录来提权。只把你信任的人加进去。
五、创建用户并授予 sudo 权限 生产服务器不建议日常用 root 操作,给每个同事建独立账号,便于审计和权限隔离。
1. 创建用户 1 2 3 4 useradd -m -s /bin/bash -c "张三" zhangsan useradd -m -s /bin/bash -c "李四" lisi useradd -m -s /bin/bash -c "王五" wangwu
参数说明:
-m 创建家目录(/home/zhangsan)
-s /bin/bash 指定登录 shell
-c "张三" 备注信息,chfn 可修改
2. 设置密码 1 2 3 passwd zhangsan passwd lisi passwd wangwu
交互式输入密码,输入时不显示字符,属正常现象 。
3. 授予 sudo 权限 1 2 3 visudo -f /etc/sudoers.d/opsusers
在打开的文件中加入(用户名按实际情况调整):
1 2 # 运维用户组,%表示该组内所有用户 %opsusers ALL=(ALL:ALL) ALL
或者直接给单个用户:
1 2 3 zhangsan ALL=(ALL:ALL) ALL lisi ALL=(ALL:ALL) ALL wangwu ALL=(ALL:ALL) ALL
保存退出(:wq)。
4. 创建运维用户组并加入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 groupadd opsusers usermod -aG opsusers zhangsan usermod -aG opsusers lisi usermod -aG opsusers wangwu id zhangsanid lisiid wangwugroups zhangsan
为什么要用组而不是逐个用户授权 :以后增减运维人员只需改组,不用动 sudoers 文件,权限管理清晰得多。
5. 验证 sudo 可用 1 2 3 4 su - zhangsan sudo whoami sudo -l exit
六、设置时区 1 2 3 4 5 timedatectl set-timezone Asia/Shanghai timedatectl
输出应包含:
1 2 Local time: Sun 2026-11-08 19:10:00 CST Time zone: Asia/Shanghai (CST, +0800)
Ubuntu 默认已经使用 systemd 的 timedatectl 管理时区,不需要 再改 /etc/localtime 或安装 tzdata。
检查时间同步:
1 2 3 4 timedatectl show -p NTPSynchronized systemctl status systemd-timesyncd
如果未同步:
1 2 apt-get install -y systemd-timesyncd systemctl enable --now systemd-timesyncd
时区对日志排查至关重要 :服务器 UTC 时间 + 本地北京时间差 8 小时,排查故障时看日志会对不上时间,这点在之前的《CentOS 错误:Local Time 与实际相差 8 小时》里已经踩过一次。
七、修改 SSH 端口为 2342 1. 修改配置文件 1 2 3 4 cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bakgrep -n "^#*Port" /etc/ssh/sshd_config
如果存在多个 Port 配置,只保留一个非注释的 ,否则 SSH 会同时监听多个端口。
编辑配置:
1 vim /etc/ssh/sshd_config
确保只有一行生效:
同时建议检查并调整这几项:
1 2 3 PermitRootLogin prohibit-password # 禁止 root 密码登录,密钥登录仍可用 MaxAuthTries 3 LoginGraceTime 30
2. 校验配置语法 这一步千万别跳过 ——配置写错会导致 SSH 服务起不来,只能进控制台救。
没有任何输出 = 配置正确。有报错就按提示改。
3. 防火墙放行新端口 1 2 3 4 5 6 7 ufw allow 2342/tcp ufw status
顺序很重要 :如果服务器启用了防火墙,必须先放行新端口,再重启 SSH ,否则会把自己关在门外。
4. 重启 SSH 并验证 1 2 systemctl restart sshd systemctl status sshd
不要关闭当前这个 SSH 窗口! 另外开一个新终端测试:
1 ssh -p 2342 zhangsan@服务器IP
新窗口能连上,才可以关闭旧窗口。
5. 配置防火墙只开放必要端口 1 2 3 4 5 6 7 ufw default deny incoming ufw default allow outgoing ufw allow 2342/tcp ufw allow 80/tcp ufw allow 443/tcp ufw enable
6. 防止忘记密码导致锁死(可选但推荐) 1 2 3 echo "Port 2222" >> /etc/ssh/sshd_configsshd -t && systemctl restart sshd
保留 2222 端口几天,确认 2342 稳定可用后再删掉这行。
八、使用 docker-compose 部署整套应用栈 本文以一个 SpringBoot 项目为例,用一份 compose 文件 把 Nginx、MySQL、Redis 和应用编排在一起。
1. 规划目录结构 所有服务统一放在 /opt 下,按服务分子目录:
1 2 mkdir -p /opt/{nginx/{conf,html,logs,certs},mysql/{data,conf,init},redis/{data,conf},app}cd /opt
最终结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /opt/ ├── docker-compose.yml # 统一编排所有服务 ├── .env # 密码等敏感配置(不提交到 Git) ├── nginx/ │ ├── conf/default.conf # Nginx 配置 │ ├── html/ # 静态文件 │ ├── logs/ # 访问/错误日志 │ └── certs/ # TLS 证书 ├── mysql/ │ ├── data/ # 数据持久化 │ ├── conf/my.cnf # MySQL 配置 │ └── init/init.sql # 初始化 SQL ├── redis/ │ ├── data/ # 持久化 │ └── conf/redis.conf # Redis 配置 └── app/ ├── Dockerfile # 基于 JDK17 的应用镜像 └── app.jar
为什么放在 /opt :上一节刚挂载的 500G 数据盘就挂在这里。数据库数据目录必须放在数据盘上,否则系统盘会被撑爆,且容器重建时数据会丢。
2. 准备证书 你已自行申请通配符证书,需要三个文件:
1 2 3 4 nginx/certs/ ├── fullchain.pem # 完整证书链(服务器上必须用这个,不是 crt) ├── privkey.pem # 私钥 └── cert.pem # 可选,部分工具需要
拷贝到服务器(在你本地机器执行):
1 scp -P 2342 fullchain.pem privkey.pem root@服务器IP:/opt/nginx/certs/
必须用 fullchain.pem :只上传 cert.pem(不含中间证书)会导致部分浏览器和移动端报证书错误——这是自签证书场景下最常见的坑。
设置权限(私钥必须是 600,容器内 Nginx 以非 root 用户运行时读不到):
1 2 chmod 600 /opt/nginx/certs/privkey.pemchmod 644 /opt/nginx/certs/fullchain.pem
证书续期 :通配符证书一般有效期较长(通常 1 年),到期前需要重新签发替换。到期时替换文件后执行 docker compose restart nginx 即可生效。
3. 准备环境变量文件 密码不要直接写在 compose 文件里。单独建一个 .env,所有服务通过变量引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cat > /opt/.env << 'EOF' MYSQL_ROOT_PASSWORD=Root@2026#Secure MYSQL_DATABASE=app_db MYSQL_USER=app MYSQL_PASSWORD=App@2026#Secure REDIS_PASSWORD=Redis@2026#Secure APP_PORT=8080 TZ=Asia/Shanghai EOF chmod 600 /opt/.env
为什么用 .env :密码集中管理、compose 文件可以进 Git 而不泄露密码、docker compose 会自动读取同目录下的 .env。
密码里的特殊字符 :.env 中 # 后面会被当成注释,$ 会被当成变量引用。所以密码里尽量避免使用这两个字符,或用引号包裹。本文示例用了 #,实际使用时建议改成不含特殊字符的强密码 ,或者用双引号包裹整行。
4. 编写 docker-compose.yml 这是整套栈的核心。所有服务加入同一个自定义网络 app-net:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 services: nginx: image: nginx:1.27-alpine container_name: nginx restart: always ports: - "80:80" - "443:443" volumes: - ./nginx/conf/default.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/html:/usr/share/nginx/html:ro - ./nginx/logs:/var/log/nginx - ./nginx/certs:/etc/nginx/certs:ro networks: - app-net depends_on: - app environment: TZ: ${TZ} healthcheck: test: ["CMD" , "wget" , "-q" , "--spider" , "http://localhost/" ] interval: 30s timeout: 5s retries: 3 mysql: image: mysql:8.0 container_name: mysql restart: always ports: - "19873:3306" environment: TZ: ${TZ} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_general_ci - --default-authentication-plugin=mysql_native_password - --lower_case_table_names=1 volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro - ./mysql/init:/docker-entrypoint-initdb.d:ro networks: - app-net healthcheck: test: ["CMD" , "mysqladmin" , "ping" , "-h" , "localhost" , "-p${MYSQL_ROOT_PASSWORD}" ] interval: 10s timeout: 5s retries: 10 start_period: 30s redis: image: redis:7-alpine container_name: redis restart: always ports: - "8971:6379" command: redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD} volumes: - ./redis/data:/data - ./redis/conf/redis.conf:/usr/local/etc/redis/redis.conf:ro networks: - app-net healthcheck: test: ["CMD" , "redis-cli" , "-a" , "${REDIS_PASSWORD}" , "ping" ] interval: 10s timeout: 5s retries: 5 app: build: context: ./app dockerfile: Dockerfile image: app-service:latest container_name: app restart: always ports: - "${APP_PORT}:8080" environment: TZ: ${TZ} SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_PORT: 6379 SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD} depends_on: mysql: condition: service_healthy redis: condition: service_healthy networks: - app-net networks: app-net: name: app-net driver: bridge
5. 关键设计说明 端口映射对照表
服务
容器内端口
宿主机端口
说明
Nginx
80 / 443
80 / 443
对外暴露,承载 HTTPS
MySQL
3306
19873
避开默认端口,减少扫描
Redis
6379
8971
同上
应用
8080
8080(可改)
建议只内网访问,由 Nginx 反代
改端口的意义 :把数据库端口从默认的 3306/6379 改掉,可以过滤掉互联网上相当一部分自动化扫描。安全上不能只依赖这个——真正的防线是密码强度 + 防火墙只放行必要端口 + 不对公网暴露 。
服务名即主机名
同一网络内,应用连数据库不需要写 IP ,直接用服务名:
1 2 SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/app_db SPRING_DATA_REDIS_HOST: redis
这是 Docker 内置 DNS 提供的服务发现能力。写 IP 的容器重启后 IP 会变,用服务名则始终稳定。
健康检查与启动顺序
depends_on 配合 condition: service_healthy,能保证 MySQL、Redis 真正就绪后 应用才启动:
1 2 3 depends_on: mysql: condition: service_healthy
只有 depends_on 而没有健康检查时,容器只是”启动了”,MySQL 还在初始化数据库,此时应用启动会因为连接失败而崩溃。
6. 配置 MySQL 创建配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 cat > /opt/mysql/conf/my.cnf << 'EOF' [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_general_ci default-time-zone = '+08:00' max_connections = 500 max_connections_error = 1000 innodb_buffer_pool_size = 2G innodb_log_file_size = 256M innodb_flush_log_at_trx_commit = 2 innodb_file_per_table = 1 slow_query_log = 1 long_query_time = 2 skip_name_resolve = 1 [client] default-character-set = utf8mb4 EOF
skip_name_resolve=1 在容器里很重要:MySQL 默认会做反向 DNS 解析,主机名解析慢时会导致连接堆积。
初始化 SQL(首次启动自动执行):
1 2 3 4 5 6 7 cat > /opt/mysql/init/init.sql << 'EOF' -- 创建业务数据库(环境变量 MYSQL_DATABASE 已自动创建,此处放额外 DDL) USE app_db; -- 授权应用账号(限制来源 IP 为应用所在网段更安全) -- FLUSH PRIVILEGES; EOF
7. 配置 Redis 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cat > /opt/redis/conf/redis.conf << 'EOF' bind 0.0.0.0protected-mode yes port 6379 appendonly yes appendfilename "appendonly.aof" appendfsync everysec dir /datamaxmemory 2gb maxmemory-policy allkeys-lru slowlog-log-slower-than 10000 slowlog-max-len 128 EOF
密码只在一处配置 :compose 里通过 --requirepass ${REDIS_PASSWORD} 传入。这样 .env 是唯一的事实来源,不会出现配置文件和 compose 两处密码不一致导致连不上。
8. 编写应用 Dockerfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cat > /opt/app/Dockerfile << 'EOF' FROM eclipse-temurin:17-jre-jammy LABEL maintainer="0x01@dosbat.com" WORKDIR /app ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY app.jar /app/app.jar EXPOSE 8080 ENTRYPOINT ["java" , \ "-Djava.security.egd=file:/dev/./urandom" , \ "-Xms512m" , "-Xmx1024m" , \ "-XX:+UseG1GC" , \ "-XX:MaxRAMPercentage=75.0" , \ "-jar" , "/app/app.jar" ] EOF
把构建好的 jar 包放进去:
1 2 3 mvn clean package -DskipTests cp target/your-app.jar /opt/app/app.jar
为什么用 -jre 而不是 -jdk :JRE 只包含运行环境,镜像体积小得多。eclipse-temurin:17-jre-jammy 约 200MB,-jdk 变体接近 500MB。除非容器里需要编译,否则一律用 JRE。
JVM 内存要和容器限制匹配 :如果给容器加了 mem_limit: 2g,那 -Xmx 不要超过它,否则 JVM 感知不到 cgroup 限制,实际用超了会被 OOM Kill。设置 MaxRAMPercentage 让 JVM 自动按比例分配更省心。
给应用容器加内存限制(在 compose 的 app 服务下补充):
1 2 3 4 5 deploy: resources: limits: memory: 2g cpus: "2.0"
9. 编写 Nginx 反向代理配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 cat > /opt/nginx/conf/default.conf << 'EOF' server { listen 80; server_name example.com *.example.com; location /.well-known/acme-challenge/ { root /var/www/html; } location / { return 301 https://$host$request_uri ; } } server { listen 443 ssl; http2 on; server_name example.com *.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_stapling on; ssl_stapling_verify on; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; client_max_body_size 20m; location / { proxy_pass http://app:8080; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; proxy_set_header X-Forwarded-Proto $scheme ; proxy_connect_timeout 30s; proxy_send_timeout 60s; proxy_read_timeout 60s; client_max_body_size 20m; proxy_buffering off; } location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf)$ { expires 30d; add_header Cache-Control "public, immutable" ; } } EOF
注意 proxy_pass 用服务名 app:8080 而不是 IP 。Nginx 和应用在同一个网络,服务名解析由 Docker DNS 完成,容器重建换 IP 后配置依然有效。
X-Forwarded-Proto 必须传,否则应用生成的跳转链接、重置密码链接会变成 http,在 https 站点下会触发混合内容问题。
10. 启动整套服务 1 2 3 4 5 6 7 8 9 10 cd /optdocker compose config docker compose up -d --build docker compose ps
启动顺序是自动的 :compose 会先启动 MySQL、Redis,等健康检查通过后再启动应用,最后启动 Nginx。
查看日志:
1 2 3 4 5 6 7 8 docker compose logs -f docker compose logs -f app docker compose logs mysql | head -50
11. 验证部署 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 docker compose ps mysql -h 127.0.0.1 -P 19873 -u root -p redis-cli -h 127.0.0.1 -p 8971 -a 'Redis@2026#Secure' ping curl -I http://127.0.0.1:8080 curl -I http://localhost curl -k -I https://localhost docker compose exec app sh -c 'echo > /dev/tcp/redis/6379 && echo "redis reachable"' docker compose exec mysql mysqladmin ping -h 127.0.0.1 -p"${MYSQL_ROOT_PASSWORD} "
12. 容器内网络连通性验证 1 2 3 4 5 6 7 8 9 10 docker network inspect app-net docker compose exec app getent hosts mysql docker compose exec app getent hosts redis docker compose exec app nc -zv mysql 3306 docker compose exec app nc -zv redis 6379
用 getent hosts mysql 能返回 IP,说明 Docker 内嵌 DNS 工作正常。如果返回空,说明服务没加入同一个网络,或服务名拼写错误。
13. 常用运维命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 cd /optdocker compose ps docker compose logs -f app docker compose restart app docker compose up -d --build app docker compose exec nginx nginx -t && docker compose exec nginx nginx -s reload docker compose down
14. 数据备份与恢复 MySQL 数据在宿主机 ./mysql/data,但直接拷贝数据目录是不安全的 (可能拷到写入中的状态)。用逻辑备份:
1 2 3 4 5 6 7 docker compose exec mysql mysqldump -u root -p"${MYSQL_ROOT_PASSWORD} " \ --all-databases --single-transaction --routines --triggers \ > /opt/backup/mysql_$(date +%Y%m%d_%H%M%S).sql docker compose exec -T mysql mysql -u root -p"${MYSQL_ROOT_PASSWORD} " < backup.sql
定时备份脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cat > /opt/backup_mysql.sh << 'EOF' BACKUP_DIR=/opt/backup mkdir -p $BACKUP_DIR cd /optsource .env docker compose exec -T mysql mysqldump -u root -p"${MYSQL_ROOT_PASSWORD} " \ --all-databases --single-transaction --routines --triggers \ > $BACKUP_DIR /mysql_$(date +%Y%m%d_%H%M%S).sql find $BACKUP_DIR -name "mysql_*.sql" -mtime +7 -delete EOF chmod +x /opt/backup_mysql.shcrontab -e
Redis 持久化已通过 appendonly yes 开启,RDB 和 AOF 都在 ./redis/data。
15. 修改密码的正确方式 密码写在 .env 里,改完只需:
1 2 3 cd /optvim .env docker compose up -d --force-recreate mysql redis app
⚠️ 改 MySQL 密码有个坑 :环境变量 MYSQL_ROOT_PASSWORD 只在首次初始化时生效 。数据库已经存在后,改这个变量不会改变现有密码,会导致应用连不上。
正确做法是进入容器执行 SQL 修改:
1 docker compose exec mysql mysql -u root -p'旧密码'
1 2 3 ALTER USER 'root' @'%' IDENTIFIED BY '新密码' ;ALTER USER 'app' @'%' IDENTIFIED BY '新密码' ;FLUSH PRIVILEGES;
然后再更新 .env 并重建应用容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 services: nginx: image: nginx:1.27-alpine container_name: nginx restart: always ports: - "80:80" - "443:443" volumes: - ./conf/default.conf:/etc/nginx/conf.d/default.conf:ro - ./html:/usr/share/nginx/html:ro - ./logs:/var/log/nginx - ./certs:/etc/nginx/certs:ro networks: - nginx-net environment: TZ: Asia/Shanghai healthcheck: test: ["CMD" , "wget" , "-q" , "--spider" , "http://localhost/" ] interval: 30s timeout: 5s retries: 3 上线前逐项检查: ```bash grep "^Port" /etc/ssh/sshd_config grep "PermitRootLogin" /etc/ssh/sshd_config ufw status verbose ufw status | grep "Default" swapon --show timedatectl | grep "Time zone" docker info | grep -A3 "Logging Driver" df -h | grep -E "/opt|/$" cd /opt && docker compose ps docker compose exec app getent hosts mysql docker compose exec app getent hosts redis ufw status | grep -E "19873|8971"
第 11 项 :MySQL(19873)和 Redis(8971)的端口映射主要是为了本地调试方便。生产环境建议不对公网开放这两个端口 ,通过防火墙只允许内网 IP 访问:
1 2 3 ufw allow from 192.168.0.0/16 to any port 19873 proto tcp ufw allow from 192.168.0.0/16 to any port 8971 proto tcp
其他建议:
定期更新系统 :apt-get update && apt-get upgrade -y
开启自动安全更新 :apt-get install -y unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades
配置 SSH 密钥登录 :禁用密码登录(PasswordAuthentication no),改用密钥
定期备份 :/etc 目录、/opt 下的 compose 配置和证书
Docker 数据目录迁移 :如果 /var/lib/docker 占用了系统盘,考虑迁移到 /opt
十、完整初始化脚本 把前面的核心步骤整合成一个脚本,新服务器可以一把梭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #!/bin/bash set -eSWAP_SIZE=8G DATA_DISK=/dev/vdb DATA_UUID="" SSH_PORT=2342 OPS_USERS="zhangsan lisi wangwu" echo "==========================================" echo " Ubuntu 24.04 服务器初始化脚本" echo "==========================================" echo "[1/8] 更新系统" apt-get update apt-get upgrade -y echo "[2/8] 设置时区" timedatectl set-timezone Asia/Shanghai echo "[3/8] 配置 ${SWAP_SIZE} Swap" if ! swapon --show | grep -q /swapfile; then fallocate -l ${SWAP_SIZE} /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab fi cat > /etc/sysctl.d/99-swap.conf << 'EOF' vm.swappiness=10 vm.vfs_cache_pressure=50 EOF sysctl -p /etc/sysctl.d/99-swap.conf > /dev/null echo "[4/8] 配置数据盘 ${DATA_DISK} " if [ -n "${DATA_UUID} " ]; then mkdir -p /opt grep -q "${DATA_UUID} " /etc/fstab || echo "UUID=${DATA_UUID} /opt ext4 defaults,noatime 0 2" >> /etc/fstab mount -a fi echo "[5/8] 安装 Docker" for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do apt-get remove -y $pkg 2>/dev/null || true done apt-get install -y docker.io docker-compose-v2 mkdir -p /etc/dockercat > /etc/docker/daemon.json << 'EOF' { "log-driver" : "json-file" , "log-opts" : { "max-size" : "100m" , "max-file" : "3" }, "storage-driver" : "overlay2" } EOF systemctl restart docker systemctl enable docker echo "[6/8] 创建运维用户" groupadd -f opsusers for u in ${OPS_USERS} ; do id ${u} > /dev/null 2>&1 || useradd -m -s /bin/bash ${u} usermod -aG opsusers ${u} usermod -aG docker ${u} done echo '%opsusers ALL=(ALL:ALL) ALL' > /etc/sudoers.d/opsuserschmod 440 /etc/sudoers.d/opsusersvisudo -c echo "[7/8] 修改 SSH 端口为 ${SSH_PORT} " cp /etc/ssh/sshd_config /etc/ssh/sshd_config.baksed -i "s/^#*Port .*/Port ${SSH_PORT} /" /etc/ssh/sshd_config sed -i 's/^#*PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config sshd -t systemctl restart sshd ufw allow ${SSH_PORT} /tcp 2>/dev/null || true ufw allow 80/tcp 2>/dev/null || true ufw allow 443/tcp 2>/dev/null || true echo "[8/8] 完成" echo "==========================================" echo " 请务必另开终端验证 SSH 连接:" echo " ssh -p ${SSH_PORT} zhangsan@$(hostname -I | awk '{print $1}') " echo " 验证成功后再关闭当前会话!" echo "=========================================="
⚠️ 执行前必看
DATA_UUID 需要你填入 blkid 查到的实际 UUID,留空则跳过磁盘挂载
务必另开一个终端 验证 SSH 能连上,再关闭当前会话
脚本用 set -e,任何一步失败会立即停止
磁盘分区(fdisk)因涉及交互,脚本中未自动化,建议手动执行
十一、常见问题 Docker 容器无法访问网络 1 2 3 4 5 iptables -L -n | grep DOCKER systemctl restart docker
修改 SSH 端口后连不上 大概率是防火墙没放行,或者 sshd_config 里有多个 Port 配置。只能通过云厂商的 VNC/控制台登录修复:
1 2 3 4 5 6 grep -n "^Port" /etc/ssh/sshd_config ufw status iptables -L -n | grep 2342
swap 创建失败 1 2 3 4 5 dd if =/dev/zero of=/swapfile bs=1M count=8192 status=progresschmod 600 /swapfilemkswap /swapfile swapon /swapfile
nginx 容器起不来看日志 1 docker compose logs nginx
常见原因:
证书路径不对 → 确认 ./certs/ 下文件名与配置一致
配置语法错误 → docker compose exec nginx nginx -t
端口被占用 → ss -lntp | grep :80
磁盘挂载后容量不对 1 2 3 4 5 6 blkid /dev/vdb2 umount /opt && mount /dev/vdb2 /opt df -h /opt
应用启动报 Connection refused(连不上 MySQL/Redis) 按顺序排查:
1 2 3 4 5 6 7 8 9 10 11 12 docker compose ps docker compose exec app getent hosts mysql docker compose exec app nc -zv mysql 3306 docker compose exec mysql mysql -u app -p'${MYSQL_PASSWORD}' -e "select 1"
90% 的情况是第 2 步 :应用容器和数据库不在同一个网络,或者 depends_on 写的是服务名但实际容器名不同。
MySQL 改了环境变量密码却不生效 1 2 3 docker compose exec mysql mysql -u root -p'旧密码'
1 2 3 ALTER USER 'root' @'%' IDENTIFIED BY '新密码' ;ALTER USER 'app' @'%' IDENTIFIED BY '新密码' ;FLUSH PRIVILEGES;
Redis 报 NOAUTH Authentication required 密码没传进去。检查 compose 里 Redis 的 command:
1 command: redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD}
验证变量是否展开:
1 docker compose config | grep requirepass
如果配置了 redis.conf 里又有 requirepass,且两个值不一致,就会连不上。密码只在一处配置 ,compose 里传参是最不容易出错的方式。
应用内存溢出被 OOM Kill 1 2 3 4 5 6 docker inspect app --format '{{.State.OOMKilled}}' dmesg -T | grep -i "killed process" docker compose exec app java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
核心原则 :JVM 的 -Xmx 不要超过容器的 mem_limit。容器限制 2G 时,-Xmx 设 1G~1.5G 更稳,或者用 -XX:MaxRAMPercentage=75.0 让 JVM 自动感知。
Nginx 502 Bad Gateway 1 2 3 4 5 6 7 8 docker compose ps app docker compose exec app sh -c "netstat -tlnp | grep 8080" docker compose logs --tail =50 app
502 说明 Nginx 活着但后端没响应。最常见是应用还在启动(SpringBoot 启动慢)或启动失败。
应用启动慢,Nginx 报 502 SpringBoot 冷启动可能要几十秒。调整 Nginx 超时和重试:
1 2 3 4 5 6 proxy_connect_timeout 30s ;proxy_send_timeout 60s ;proxy_read_timeout 60s ;proxy_next_upstream error timeout http_502 http_503;
同时给应用配置健康检查,让 Nginx 能感知就绪状态:
1 2 3 4 5 6 healthcheck: test: ["CMD" , "curl" , "-f" , "http://localhost:8080/actuator/health" ] interval: 15s timeout: 5s retries: 5 start_period: 60s
数据卷权限问题(MySQL 起不来) 1 2 3 4 5 6 docker compose logs mysql | tail -20 chown -R 999:999 /opt/mysql/data
compose 文件里密码被覆盖 1 2 3 4 5 docker compose config ls -la /opt/.env
.env 必须在 docker-compose.yml 同级目录 才会被自动加载。放在子目录里不会生效。
总结 一台服务器从裸机到能跑生产服务,完整链路是:
磁盘分区 → Swap → 系统更新 → 时区 → Docker → 用户与权限 → SSH 加固 → 容器化部署 → 安全检查
每个环节都有对应的坑:
环节
关键点
Swap
权限必须 600;vm.swappiness 调低到 10
磁盘
fdisk 交互要点;用 UUID 挂载;先 mkfs 再挂载
Docker
必须限制日志大小 ;国内建议配镜像加速
用户
用组统一授权 sudo;visudo 避免语法错误
SSH
先验证新端口再关旧窗口 ;改端口前先放行防火墙
Nginx
用 fullchain.pem 而非 cert.pem;配置改完先 nginx -t
MySQL
改密码要走 SQL,环境变量只在首次初始化生效
Redis
密码只在一处配置 ,避免 compose 与 conf 不一致
应用
-Xmx 不能超过容器内存限制;JRE 而非 JDK 镜像
把这套流程固化下来,之后每上一台新机器,半小时内就能完成基础环境搭建,业务部署就是后面的事了。
整套栈的网络拓扑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 公网 │ ┌────▼────┐ │ Nginx │ 80 / 443 (HTTPS) └────┬────┘ │ proxy_pass http://app:8080 ┌────────────────┼────────────────┐ │ │ │ ┌────▼────┐ ┌─────▼─────┐ ┌────▼────┐ │ app │ │ MySQL │ │ Redis │ │ JDK17 │ │ 19873 │ │ 8971 │ │ :8080 │────▶│ :3306 │ │ :6379 │ └─────────┘ └───────────┘ └─────────┘ │ │ │ └────────────────┴────────────────┘ app-net 网络 (服务名即主机名,免 IP)
记住这条原则 :容器间用服务名互连(mysql:3306),只有 Nginx 对外暴露 80/443,数据库端口尽量只在内网可达。
相关阅读