ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker+nginx反向代理实战:从安装到容器化部署完整指南

Docker+nginx反向代理实战:从安装到容器化部署完整指南 搞 Web 开发或者运维的同学肯定绕不开两样东西Docker 和 nginx。这两个单拎出来都不简单但把它们凑在一起做反向代理反而是日常开发里最常用、性价比最高的组合。今天这篇文章我就从一个实际落地的角度带你把“安装 Docker”到“用 nginx 容器做反向代理”这条链路完整走一遍过程中所有命令我都会解释为什么要这么写不搞“复制粘贴跑通就算完”那套。先说清楚这篇文章适合谁想入门 Docker 的开发者、刚开始折腾服务器部署的运维新人、以及那些被“容器化部署”绕晕的团队新人都可以照着这篇文章一步步操作。就算你之前完全没碰过 Docker只要能看懂基础命令行跟着走完就能理解容器、镜像、端口映射、挂载目录、反向代理这几个核心概念到底是怎么串起来的。1. 先搞清楚反向代理到底解决什么问题1.1 为什么你的项目需要一个“统一入口”咱们先不聊技术聊一个特别常见的场景。你本地起了好几个服务前端开发服务器在 5173 端口后端接口在 8080 端口可能还有个管理后台在 8888 端口。现在你要给同事演示或者部署到服务器上总不能让人家记住一串端口号吧更麻烦的是如果以后服务多了HTTPS 证书、负载均衡、日志记录、请求限制这些需求都会冒出来。你总不能在每个服务里都重复实现一遍这些东西。反向代理解决的就是这个“统一入口”的问题。它把所有外部请求先接收到自己这里再根据规则转发给后面的真实服务。对外你只需要暴露一个 80 或 443 端口对内你想怎么转发都行。nginx 在这个领域是最老牌的工具之一配置灵活、性能强文档也丰富。1.2 Docker 在这个场景里扮演什么角色你可能要问既然 nginx 可以安装到宿主机上为什么还要用 Docker 再套一层我的答案是为了可复制性和隔离性。用 Docker 跑 nginx你的宿主机不会因为装了一堆依赖而越来越乱。nginx 的配置、版本、运行环境全部被塞进一个镜像里想换版本就换 tag想回滚就用旧镜像重新起一个容器全程不污染系统。换一台服务器时你不需要重新踩一遍“编译安装 nginx”的坑直接拉镜像起来就是一模一样的环境。这个特性在团队协作里尤其香。你提交一个 docker-compose.yml 文件同事一条命令就能把整套代理环境拉起来再也不用对着文档手动配半天。2. 把 Docker 环境装好这是所有操作的前提2.1 Windows / macOS 用户直接上 Docker DesktopWindows 和 macOS 没有太好的原生容器方案Docker Desktop 是官方推荐的工具它内部帮你管好了虚拟机、容器运行时、命令行工具这些杂事安装完就能用。Docker Desktop 安装其实没有太多花活官网下载安装包一路点下一步就行。但有两个前置条件容易踩坑Windows 需要开启 WSL2并且系统要求是 Win10 64 位以上版本。如果你的机器 BIOS 里没开虚拟化Docker Desktop 启动时会直接报错。我第一次在 Windows 上装 Docker Desktop 时就卡在 WSL2 上一直提示“Please enable WSL2”最后是跑到“控制面板 - 程序 - 启用或关闭 Windows 功能”里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”重启电脑才搞定。macOS 这边简单一些Intel 芯片和 Apple Silicon 芯片下载的安装包不同别下错了。安装完成后打开终端跑一句docker --version docker compose version能输出版本号就说明基本环境没问题了。2.2 Linux 用户别用 Desktop直接用 EngineLinux 上没有必要装 Docker Desktop装 Docker Engine 就够了。Ubuntu / Debian 系可以用官方安装脚本一步到位curl -fsSL https://get.docker.com | bash国内网络环境下官方脚本有时拉取慢可以改用国内镜像源手动安装原理就是把官方源替换成清华或阿里的源然后apt install docker-ce。CentOS / RHEL 系则是yum install docker-ce流程类似。装完以后有个必做动作把当前用户加进 docker 组。否则每次执行 docker 命令都得带sudo特别烦。sudo usermod -aG docker $USER newgrp docker这一步完成后再执行docker ps如果不用 sudo 也能正常运行环境就彻底通了。2.3 镜像拉取慢先配好镜像加速很多人第一次用 Docker 都会遇到同一个问题docker pull nginx卡了半天不动进度条纹丝不动。这不是你网络坏了而是 Docker 默认从 Docker Hub 拉镜像从某些地区访问 Docker Hub 确实非常慢。解决办法是配置“镜像加速器”。国内各个云厂商都提供了免费的 Registry Mirror原理是在你拉镜像时自动从离你更近的镜像仓库同步。配置位置在 Docker Desktop 的设置里或者 Linux 下的/etc/docker/daemon.json{ registry-mirrors: [ https://docker.m.daocloud.io, https://docker.xuanyuan.me ] }改完以后重启 Docker 服务Linux 下用systemctl restart dockerWindows / macOS 直接在 Docker Desktop 里重启即可。之后再拉 nginx 镜像速度会快非常多。需要提醒一句镜像加速器地址经常变化如果发现某个地址失效了去搜一下当前可用的公共加速地址替换即可。3. 用 Docker 跑起第一个 nginx 容器3.1 从拉镜像到访问页面只需要三条命令环境就绪之后我们来跑第一个 nginx 容器。整个操作只需要三步docker pull nginx这条命令会从镜像仓库拉取 nginx 最新版镜像。如果上一步你配置好了加速这里应该几十秒就能完成。docker run --name web -p 80:80 -d nginx这条命令的参数我逐个拆解一下--name web给容器起个名字后面管理容器时直接用名字不需要记一串容器 ID。-p 80:80端口映射。冒号左边是宿主机端口右边是容器内部端口。含义是“宿主机上的 80 端口收到的请求转发到容器内部的 80 端口”。为什么容器内部是 80因为 nginx 镜像默认监听 80 端口。-d后台运行终端不卡住。启动之后验证一下curl http://localhost:80正常情况下你会看到一串 HTML里面写着 “Welcome to nginx!” 这说明容器已经成功跑起来了。如果你看到的是这个页面你的 Docker 入门第一步已经完成了。3.2 为什么端口映射和目录挂载是 Docker 的核心操作容器本身是一个“隔离的环境”外面的世界访问不到容器内部容器也访问不到外面除非你主动开了一扇门。端口映射-p就是那扇门。再看目录挂载。默认的 nginx 页面内容在容器里的/usr/share/nginx/html目录。你想部署自己的网站总不能每次跑容器都用docker exec进到容器里改文件吧那是纯折腾自己。正确做法是把宿主机的目录挂载进去用-v参数docker run --name web \ -p 80:80 \ -v /home/user/www:/usr/share/nginx/html \ -v /home/user/nginx/conf:/etc/nginx/conf.d \ -d nginx这条命令的意思是把宿主机的/home/user/www目录映射到容器里的网站根目录把本机/home/user/nginx/conf目录映射到 nginx 的配置目录。这样你改本机文件容器里的内容就同步变了。做开发的应该能马上理解这个好处改完代码不用重新打镜像、不用重建容器。这里我要重点提醒一个坑目录挂载的权限问题。如果宿主机目录的属主和属组跟容器内 nginx 进程的用户不一致很可能会出现 403 Forbidden。我遇到过很多次挂载目录后打开页面是 403进容器一查是宿主机目录权限是 755 但属主是 root而 nginx worker 进程用的是 nginx 用户读不了文件。解决办法是把宿主机目录权限改成 755 且允许其他用户读取或者用--user参数指定容器运行用户再或者干脆把目录属主改成 uid 101nginx 官方镜像默认用户的 uid。3.3 nginx 配置文件在容器里应该怎么管理nginx 官方镜像内部的目录结构跟宿主机编译安装的 nginx 基本一致主配置文件在/etc/nginx/nginx.conf站点配置在/etc/nginx/conf.d/目录下。默认的 nginx.conf 已经写好了 include 逻辑会把/etc/nginx/conf.d/*.conf下的文件全部加载进来。所以日常管理配置最推荐的做法是新增一个default.conf放到挂载的conf目录下不需要去动主配置文件。修改完配置要让 nginx 重新加载配置有两个层面容器层面docker exec web nginx -s reload让容器里的 nginx 重新读配置。如果你的 nginx 配置改了容器端口或 conf 加载路径那必须重建容器。注意我见过很多新人改完配置后不 reload甚至直接docker restart web这也不是不行但 restart 会中断服务而 reload 是平滑的生产环境推荐用 reload。4. 实战配置 nginx 反向代理4.1 一个典型场景后端服务在 8080通过 nginx 暴露到 80现在进入正题配置反向代理。我先给一个最经典的场景你的后端服务跑在宿主机的 8080 端口现在你想让用户通过 nginx 的 80 端口访问不需要用户记http://ip:8080这种带端口的地址。先准备一个配置文件/home/user/nginx/conf/default.confserver { listen 80; server_name _; location / { proxy_pass http://127.0.0.1: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; } }然后重建容器把 conf 目录挂载进去docker run --name web \ -p 80:80 \ -v /home/user/nginx/conf:/etc/nginx/conf.d \ -d nginx访问http://localhost你会看到后端服务返回的内容。这里最关键的一个知识点是proxy_pass后面的地址。我在 Linux 上用127.0.0.1:8080能通吗答案是在 Linux 上不一定通这又是个经典坑。因为容器有自己的网络命名空间容器里的127.0.0.1指的是容器自己不是宿主机。要在容器里访问宿主机的服务需要区分操作系统Windows / macOSDocker Desktop用http://host.docker.internal:8080Docker Desktop 会自动把host.docker.internal解析到宿主机。Linux需要先查宿主机在 Docker 网络里的 IP一般是172.17.0.1或者用--network host运行容器让容器直接使用宿主机网络栈此时127.0.0.1就是宿主机。我个人的建议是在 Linux 上做这种代理到宿主机服务的场景直接用--network host启动 nginx 容器一了百了不用纠结 IP。代价是端口映射参数-p就不需要了因为容器直接共享宿主机网络。4.2 代理到 Docker 网络内的另一个容器更常见的场景是不只 nginx 跑在 Docker 里你的后端服务也跑在 Docker 里。这时候比宿主机 IP 更优雅的方案是让容器之间通过“网络”互相通信。先创建一个自定义网络docker network create webnet然后启动两个容器都加入到这个网络docker run --name backend --network webnet -d backend-image docker run --name web \ --network webnet \ -p 80:80 \ -v /home/user/nginx/conf:/etc/nginx/conf.d \ -d nginx这时候在 nginx 容器里可以用backend这个容器名直接访问后端服务。配置改成location / { proxy_pass http://backend:8080; ... }为什么能直接用容器名当域名因为 Docker 内置了 DNS 解析在同一个自定义网络里容器名会被自动解析成对应容器的 IP。这就是容器编排里服务发现的基础机制。对比一下--link是老版本 Docker 的临时方案现在基本可以不用了。自定义网络不仅支持容器名解析还支持网络隔离体验好太多。4.3 多站点场景按域名分流真实项目里不可能只有一个服务。你可能有个前端站点、一个后端 API、一个管理后台。用 nginx 做反向代理最常见的方式是按域名分流。假设你有三个域名www.example.com对应前端静态文件直接用 nginx 返回。api.example.com对应后端 API转发到 backend 容器。admin.example.com对应管理后台转发到 admin 容器。配置文件可以这样写server { listen 80; server_name www.example.com; root /usr/share/nginx/html; index index.html; } server { listen 80; server_name api.example.com; location / { proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } server { listen 80; server_name admin.example.com; location / { proxy_pass http://admin:3000; proxy_set_header Host $host; } }每个server块根据server_name匹配不同的域名。DNS 解析需要把这三个域名都指向你的服务器公网 IPnginx 收到请求后根据 Host 头决定转发给谁。4.4 按路径转发一个域名管多个服务不是所有人都有多个域名。如果你只有一个域名也可以通过路径区分服务server { listen 80; server_name example.com; location /api/ { proxy_pass http://backend:8080/; } location /admin/ { proxy_pass http://admin:3000/; } location / { root /usr/share/nginx/html; index index.html; } }这里有一个细节容易踩坑proxy_pass后面的 URL 是否带结尾的/会影响转发时的路径拼接方式。proxy_pass http://backend:8080;请求/api/user转发的完整路径不变。proxy_pass http://backend:8080/;location /api/中匹配到的/api/部分会被替换成/所以/api/user变成/user再转发过去。后端如果定义的路由是/user那第二种写法才是对的。搞反了就会出现 404而且你一时半会儿还找不到原因。5. 引入 Docker Compose让编排更简单5.1 为什么长命令撑不住复杂场景前面我们用了docker run启动 nginx 容器。单独一个服务还好但当你需要同时启动 nginx、backend、admin、数据库每个容器都要配端口、挂目录、加网络命令会越来越长而且不好维护。Docker Compose 就是来解决这个问题的。它用一个docker-compose.yml文件描述所有服务一条命令启动一条命令停止。5.2 一个完整的 compose 文件示例假设我们有两个服务nginx 和 backend。目录结构如下project/ ├── docker-compose.yml ├── nginx/ │ └── default.conf └── backend/ └── Dockerfiledocker-compose.yml内容version: 3.8 services: backend: build: ./backend container_name: backend networks: - webnet environment: - NODE_ENVproduction nginx: image: nginx:1.27 container_name: web ports: - 80:80 volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf - ~/www:/usr/share/nginx/html depends_on: - backend networks: - webnet networks: webnet: driver: bridge几个点说一下ports代替了命令行里的-p。volumes代替了-v而且路径是相对于 compose 文件的相对路径。networks定义了一个自定义网络Compose 会自动把所有加入该网络的服务名解析到对应容器。depends_on表示 nginx 依赖 backend启动时先启动 backend。不过注意depends_on只保证启动顺序不保证 backend 内部服务已经 ready。生产环境需要用 healthcheck 配合。启动命令就一句docker compose up -d查看日志、停止服务、彻底删除服务docker compose logs -f nginx docker compose stop docker compose downdown会删除容器和网络但不会删除挂载的数据卷所以不用担心数据丢失。5.3 为什么我建议你从第一天就上 Compose很多教程喜欢从docker run教起但我自己的体会是只要你的项目有两个以上容器或者你的 nginx 配置要跟项目一起版本管理直接用 Compose 会让你少走很多弯路。原因很朴素docker run命令里的所有参数都是临时的你不可能把它提交到 Git。而docker-compose.yml是代码可以放进仓库新同事 clone 下来直接up -d就能获得一模一样的环境。这不正是我们用 Docker 的初衷吗6. 常见问题与排查技巧实录6.1 端口被占用bind: address already in use启动容器时最常遇到的就是端口冲突。现象是docker run报错提示bind: address already in use。排查方法lsof -i :80 netstat -tlnp | grep :80找到占用 80 端口的进程要么停掉它要么换 nginx 的宿主端口。如果你本机已经装了 Apache它默认占用 80大概率就是它跟 nginx 打起来了。还有一个小技巧如果端口被另一个 Docker 容器占用用docker ps -a看一下找到旧容器删掉就行。6.2 502 Bad Gateway上游服务不通504 和 502 是反向代理时代最常见的错误。502 Bad Gateway 表示 nginx 转发请求后上游服务没响应或连不上。排查顺序看 nginx 配置里的proxy_pass地址对不对。确认上游服务容器有没有启动docker ps。在 nginx 容器里手动 curl 一下上游地址docker exec web curl http://backend:8080。确认网络对不对两个容器要在一个自定义网络里才能用服务名解析。我之前帮同事排查过一个案例nginx 配置完全正确但一直 502。最后发现是 backend 容器启动后马上退出了因为启动命令少了参数进程根本没起来。docker ps -a能清楚地看到容器的退出状态码和启动日志排查容器问题第一个动作永远是先看日志。6.3 404 / 403挂载目录的权限和路径坑403 的问题之前提到了通常是权限。404 则是路径配置问题。常见场景你挂载了宿主机目录打开页面提示 403那是因为容器内 nginx 用户没有宿主机目录的读取权限。对于 CentOS / Ubuntu 服务器场景很多人的家目录默认权限是 700容器里 nginx 用户自然是没法访问的。解决办法把网站目录放到权限更开放的位置或者用chmod -R 755调整目录权限。404 多半是root路径写错了。注意 nginx 的root是“把 URL 拼接到 root 后面找文件”如果你把网站文件放在/usr/share/nginx/html/index.htmlroot 就应该是/usr/share/nginx/html别写成/usr/share/nginx否则会去/usr/share/nginx/index.html找自然 404。6.4 配置修改后不生效改了挂载目录里的default.conf刷新页面没反应。99% 的情况是没有 reload。正确的姿势docker exec web nginx -t docker exec web nginx -s reload先执行nginx -t检查语法语法错误的话 reload 会失败而且你还不容易发现。这个习惯一定要养成生产环境上直接 reload 一个语法错误的配置后果是 nginx 直接拒绝加载配置服务可能中断。6.5 镜像拉取失败或一直拉不下来除了配置加速器还有几个进阶手段换 tagdocker pull nginx:alpine比docker pull nginx体积小很多也能避免某些大镜像因为分层多而失败。如果公司有 Harbor 或 Nexus 私有仓库让运维把基础镜像同步到内网仓库拉取速度最快。本地构建时尽量使用带 alpine 后缀的镜像体积小、拉取快、漏洞也少。我自己个人偏好就是所有能用 alpine 的镜像全部选 alpine不仅拉取快磁盘占用也小对服务器来说省下的是实打实的空间。最后再分享一点我的实际感受Docker 和 nginx 这套组合真正难的不是某个命令怎么写而是理解“容器是隔离的”、“网络是可自定义的”、“配置是可以通过挂载和版本管理进项目里的”这几个观念。一旦你把 Docker 的镜像、容器、数据卷、网络这四个核心概念想透了后续不管是玩 Kubernetes 还是做 CI/CD都会顺利很多。这篇文章里我刻意绕开了很多进阶话题比如 HTTPS 证书、负载均衡、WebSocket 支持、日志轮转但你在掌握了基础之后完全可以自己去扩展。在我的实际使用中nginx 反向代理 Docker 的这套方案无论放在本地开发还是生产环境都是非常稳的组合。建议你找一台测试服务器亲手敲一遍这里的命令遇到 502、403、404 不要慌按上面提到的排查路径一步步来这些坑踩过一次后面就再也不会犯了。
返回列表