从零在华为云 ECS 上部署 Docker + Nginx 站点并配置 HTTPS:一次踩坑全记录
一台 2 核 4G 的 ECS,从裸机到跑起一个带 HTTPS 的静态站点,全程用 Docker Compose 管理。本文把每一步的命令、配置和踩过的坑都记下来,照着做大概 30 分钟能复现。
写在前面
很多人第一次上华为云 ECS 会卡在几个地方:安全组没放行端口、容器里解析不到域名、证书续期后 Nginx 没重载。这篇文章按「实例 → 系统 → Docker → 站点 → HTTPS」的顺序走一遍,每一步都给出可直接复制的命令,最后单独一节列坑。
环境约定:
- 系统:Ubuntu 22.04 LTS
- 规格:2 vCPU / 4 GiB(静态站 + 反向代理完全够用)
- 部署方式:Docker Compose,Nginx 容器对外,静态文件挂载进容器
- 证书:acme.sh 申请 Let's Encrypt,自动续期
一、实例与网络:先把路打通
创建 ECS 时有两件事必须在开机前想清楚,否则后面全是玄学问题。
1. 安全组要一次配齐。 入方向至少放行:
| 端口 | 协议 | 来源 | 用途 |
|---|---|---|---|
| 22 | TCP | 你的公网 IP/32 | SSH 管理 |
| 80 | TCP | 0.0.0.0/0 | HTTP 与证书签发校验 |
| 443 | TCP | 0.0.0.0/0 | HTTPS |
这里建议 22 端口只放行自己的出口 IP,不要图省事开 0.0.0.0/0,公网上的爆破流量会比你想象的多得多。
2. 带宽按峰值估。 静态站点的瓶颈通常在带宽而不是 CPU。1 Mbps 约等于 128 KB/s,如果你的首页有 1 MB 静态资源,首屏就要 8 秒——这种情况下压缩和 CDN 比升配置更划算。
创建完成后绑定弹性公网 IP(EIP),记下公网地址。
二、系统初始化:把地基打平
第一次登录用 root + 密钥。进去之后第一件事不是装软件,是把 root 密码登录关掉。
# 新建运维用户并授权
adduser deploy
usermod -aG sudo deploy
# 把公钥复制过去
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
# 禁用 root 密码登录与密码认证
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd
改完配置先别断开当前会话,另开一个终端用
deploy账号登录验证成功,再关掉旧连接。这一步能救回不少被锁在门外的服务器。
接着做几项常规设置:
# 时区
timedatectl set-timezone Asia/Shanghai
# 系统更新
apt update && apt -y upgrade
# 2C4G 建议加 2G swap,防止构建镜像时 OOM
fallocate -l 2G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
三、安装 Docker 与 Compose
用 Docker 官方脚本最省事,它会自动装好 docker compose 插件:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
装完先配一下日志轮转。默认情况下容器日志会无限增长,几个月后能把磁盘吃满:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" },
"registry-mirrors": ["https://<你的镜像加速地址>"]
}
EOF
sudo systemctl restart docker
验证:
docker --version
docker compose version
docker run --rm hello-world
四、用 Compose 编排 Nginx 与站点
目录结构保持干净,后面扩服务会轻松很多:
/srv/site
├── docker-compose.yml
├── nginx/
│ └── conf.d/
│ └── site.conf
├── html/
│ └── index.html
└── certs/
docker-compose.yml:
services:
web:
image: nginx:1.27-alpine
container_name: site-web
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./html:/usr/share/nginx/html:ro
- ./certs:/etc/nginx/certs:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/healthz"]
interval: 30s
timeout: 3s
retries: 3
nginx/conf.d/site.conf,重点在压缩、缓存和安全头:
server {
listen 80;
server_name example.com;
# 证书签发校验用,务必保留
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1k;
# 带指纹的静态资源长缓存
location ~* \.(js|css|png|jpg|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# HTML 不缓存,保证发版即时生效
location = /index.html {
add_header Cache-Control "no-cache";
}
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header Referrer-Policy strict-origin-when-cross-origin;
location = /healthz { return 200 "ok"; }
}
启动:
cd /srv/site
docker compose up -d
docker compose ps
curl -I http://127.0.0.1
五、HTTPS:签发与自动续期
Let's Encrypt 证书 90 天有效期,必须做自动续期,否则第 91 天站点直接挂。
用 webroot 模式签发,不需要停 Nginx:
curl https://get.acme.sh | sh -s email=you@example.com
~/.acme.sh/acme.sh --issue \
-d example.com -d www.example.com \
-w /srv/site/html --server letsencrypt
~/.acme.sh/acme.sh --install-cert -d example.com \
--key-file /srv/site/certs/privkey.pem \
--fullchain-file /srv/site/certs/fullchain.pem \
--reloadcmd "docker exec site-web nginx -s reload"
--reloadcmd 是关键:acme.sh 续期成功后会自动执行它,让 Nginx 加载新证书。少了这一行,证书文件更新了但 Nginx 还在用旧的,到期那天才会发现。
acme.sh 安装时会自动写入 crontab,检查一下:
crontab -l | grep acme
六、踩坑记录
1. 安全组放行了但还连不上。 除了安全组,Ubuntu 自带的 ufw 也可能拦着。sudo ufw status 看一眼,需要的话 sudo ufw allow 443/tcp。
2. 容器里 ping 不通外网、解析不了域名。 多半是宿主机 DNS 配置的问题,容器继承 /etc/resolv.conf。检查 resolvectl status,必要时在 compose 里显式指定 dns: [223.5.5.5, 1.1.1.1]。
3. 证书续期成功但浏览器仍报过期。 就是上面说的 --reloadcmd 没配。手动补救:docker exec site-web nginx -s reload。
4. docker compose up 报端口被占用。 检查是否装过系统自带的 nginx/apache:ss -lntp | grep :80,有的话 systemctl disable --now nginx。
5. 磁盘写满但 du 看不出来。 容器日志和已删除但仍被进程占用的文件不占 du 的统计。用 df -h 看分区,lsof +L1 找已删除的句柄。
七、成本与收尾
这套配置的资源占用很低:Nginx 容器常驻内存约 10 MB,2C4G 的实例跑一个中小流量站点绰绰有余。真正要留意的是带宽和 EIP 的计费方式——按流量计费适合波峰明显的场景,按带宽计费适合持续有流量的站点,长期跑建议先观察一周的实际出流量再决定。
另外建议把 html/ 目录纳入 Git 管理,用 CI 构建后 rsync 到服务器,比手工传文件可靠得多:
rsync -avz --delete dist/ deploy@<EIP>:/srv/site/html/
小结
整个流程的核心其实就三件事:网络先通、配置集中管理、证书续期要有 reload 钩子。把这三件事做对,剩下的都是 Nginx 配置的细节问题。Docker Compose 的价值在于把「服务器上装了什么」变成一份可以进 Git 的文件,换台机器 up -d 就能复现——这比记住一堆手工命令重要得多。
- 点赞
- 收藏
- 关注作者
评论(0)