Docker+Nginx单副本零停机发布实现并解决文件访问 404

 更新时间:2026年04月28日 09:06:27   作者:韩zj  
本文提供了解决Docker容器文件隔离问题的方法,核心是Nginx和业务容器挂载相同的宿主机文件目录,通过Nginx重写规则去掉/api前缀匹配业务服务路径,同时发布时流量同步切换到临时副本,避免404和中断问题

按照上一篇文章部署以后,网页和接口访问正常,但是访问文件是404,目前是解决了,给出通用的解决方案。
所有敏感信息(IP / 域名 / 密码)均用占位符标识,你只需替换成自身环境值即可直接使用,专门解决「IP 访问文件正常、域名访问 404」「发布切换副本文件访问中断」问题。

一、核心问题复现

  • 正常访问:http://xxx.xxx.xxx.xxx:8080/profile/upload/test.bin
  • 异常访问:https://your-domain.com/api/profile/upload/test.bin(404)
  • 核心原因:Docker 容器文件隔离 + Nginx 路径映射不匹配 + 发布流量切换未同步文件访问

二、可直接复制的配置模板(全量)

  1. docker-compose.yml(替换占位符即可用,根据自己实际情况修改)
version: "3.9"
services:
    redis:
        image: bitnami/redis:latest
        hostname: redis
        container_name: redis
        restart: always
        privileged: true
        ports:
            - 6379:6379
        environment:
            REDIS_PASSWORD: your-redis-password  # 替换:你的Redis密码
            TZ: Asia/Shanghai
        volumes:
            - type: volume
              source: redis-data
              target: /bitnami/redis/data
              volume: { }
        networks:
            - app-web-net
    nginx:
        image: nginx:latest
        container_name: nginx
        restart: always
        ports:
            - 80:80
            - 443:443
        privileged: true
        networks:
            - app-web-net
        volumes:
            - /root/nginx/nginx.conf:/etc/nginx/nginx.conf
            - /root/nginx/conf.d/:/etc/nginx/conf.d/
            - /root/nginx/html/:/usr/share/nginx/html/
            - /root/nginx/logs/:/var/log/nginx/
            - /root/nginx/cert/:/etc/nginx/cert/
            # 核心:挂载文件目录(替换:你的宿主机文件目录)
            - /root/your-app/uploadPath/:/root/your-app/uploadPath/
    # 主业务服务(8080端口)
    app-main:
        image: your-app-image:1.0.0  # 替换:你的业务镜像
        restart: always
        privileged: true
        depends_on:
            - redis
        ports:
            - "8080:8080"
        environment:
            MYSQL_USER_NAME: your-db-username  # 替换:数据库用户名
            MYSQL_PWD: your-db-password        # 替换:数据库密码
            MYSQL_DB_NAME: your-db-name        # 替换:数据库名
            MYSQL_HOST: xxx.xxx.xxx.xxx        # 替换:数据库IP
            MYSQL_PORT: 3306
            REDIS_HOST: redis
            REDIS_PORT: 6379
            REDIS_PWD: your-redis-password     # 替换:Redis密码(和上方一致)
            SERVER_PORT: 8080
            UPLOAD_PATH: /home/your-app/uploadPath/  # 容器内文件目录
            TZ: Asia/Shanghai
        volumes:
            # 挂载文件目录(和Nginx挂载的宿主机目录一致)
            - /root/your-app/uploadPath/:/home/your-app/uploadPath/
        networks:
            - app-web-net
        healthcheck:
            test: ["CMD", "curl", "-f", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080/api/health"]
            interval: 5s
            timeout: 3s
            retries: 10
            start_period: 60s
    # 临时副本(发布兜底,8081端口)
    app-temp:
        image: your-app-image:1.0.0  # 替换:和主服务镜像一致
        restart: "no"
        privileged: true
        depends_on:
            - redis
        ports:
            - "8081:8080"
        environment:
            MYSQL_USER_NAME: your-db-username  # 替换:和主服务一致
            MYSQL_PWD: your-db-password        # 替换:和主服务一致
            MYSQL_DB_NAME: your-db-name        # 替换:和主服务一致
            MYSQL_HOST: xxx.xxx.xxx.xxx        # 替换:和主服务一致
            MYSQL_PORT: 3306
            REDIS_HOST: redis
            REDIS_PORT: 6379
            REDIS_PWD: your-redis-password     # 替换:和主服务一致
            SERVER_PORT: 8080
            UPLOAD_PATH: /home/your-app/uploadPath/
            TZ: Asia/Shanghai
        volumes:
            - /root/your-app/uploadPath/:/home/your-app/uploadPath/
        networks:
            - app-web-net
        healthcheck:
            test: ["CMD", "curl", "-f", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080/api/health"]
            interval: 5s
            timeout: 3s
            retries: 10
            start_period: 60s
volumes:
    redis-data:
        name: app-redis-data
        external: true
networks:
  app-web-net:
    driver: bridge
  1. Nginx 配置文件(conf.d/default.conf)
# ===================== 全局核心配置 =====================
resolver 127.0.0.11 valid=30s;

proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 1s;

# 主服务集群(发布切换核心)
upstream app-servers {
    least_conn;
    server app-main:8080 max_fails=1 fail_timeout=1s;
    keepalive 32;
}

proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
proxy_buffering on;

gzip  on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
tcp_nopush      on;
tcp_nodelay     on;

# ===================== 1. HTTP 跳转HTTPS =====================
server {
    listen 80;
    # 替换:你的服务器IP + 域名
    server_name xxx.xxx.xxx.xxx your-domain.com;
    return 301 https://$host$request_uri;
}

# ===================== 2. HTTPS 核心配置 =====================
server {
    listen 443 ssl;
    # 替换:你的域名
    server_name your-domain.com;

    # 替换:你的SSL证书路径
    ssl_certificate /etc/nginx/cert/your-domain.com.pem;
    ssl_certificate_key /etc/nginx/cert/your-domain.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_tickets off;
    ssl_session_timeout 1d;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_prefer_server_ciphers on;

    # ===================== 解决文件404核心配置 =====================
    # 固件/上传文件访问:/api/profile/upload/xxx → 代理到/app-main/profile/upload/xxx
    location ^~ /api/profile/upload/ {
        rewrite ^/api/profile/upload/(.*)$ /profile/upload/$1 break;
        proxy_pass http://app-servers;
        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;
        add_header Access-Control-Allow-Origin *;
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
    }

    # 头像文件访问:/api/profile/avatar/xxx → 代理到/app-main/profile/avatar/xxx
    location ^~ /api/profile/avatar/ {
        rewrite ^/api/profile/avatar/(.*)$ /profile/avatar/$1 break;
        proxy_pass http://app-servers;
        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;
        add_header Access-Control-Allow-Origin *;
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
    }

    # Swagger文档代理(如有)
    location ~ ^/api/(swagger-ui|v3/api-docs|swagger-resources|webjars) {
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass http://app-servers;

        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_set_header Accept-Encoding "";

        sub_filter 'https://your-domain.com:443/api' 'https://your-domain.com/api';
        sub_filter 'https://your-domain.com:443' 'https://your-domain.com/api';
        sub_filter '"url":"https://your-domain.com"' '"url":"https://your-domain.com/api"';
        sub_filter '"url": "https://your-domain.com"' '"url": "https://your-domain.com/api"';
        sub_filter 'https://your-domain.com/' 'https://your-domain.com/api/';
        sub_filter 'https://your-domain.com/api/api/' 'https://your-domain.com/api/';

        sub_filter_once off;
        sub_filter_types application/json text/html text/javascript;

        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }

    # 前端页面访问
    location / {
        root   /usr/share/nginx/html/your-app;  # 替换:你的前端文件目录
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    # 主API接口代理
    location /api/ {
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass http://app-servers;

        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_set_header X-Forwarded-Prefix /api;
        proxy_set_header X-Forwarded-Port "";

        proxy_http_version 1.1;
        proxy_set_header Connection "";

        proxy_set_header Accept-Encoding "";
        sub_filter 'http://your-domain.com/profile/upload/' 'https://your-domain.com/api/profile/upload/';
        sub_filter 'https://your-domain.com/profile/upload/' 'https://your-domain.com/api/profile/upload/';
        sub_filter_once off;
        sub_filter_types application/json;
    }

    # 下载路径代理
    location /download {
        rewrite ^/download/(.*)$ /profile/upload/$1 break;
        proxy_pass http://app-servers;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        add_header Access-Control-Allow-Origin *;
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
    }

    # 安全头配置
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
  1. 零停机发布脚本(deploy_app.sh)
#!/bin/bash
set +e

# ===================== 替换以下占位符 =====================
MONITOR_URL="https://your-domain.com/api/app/checkServerStatus"  # 你的健康检查接口
MAIN_CHECK_URL="http://xxx.xxx.xxx.xxx:8080/api/app/checkServerStatus"  # 主服务健康检查
TEMP_CHECK_URL="http://xxx.xxx.xxx.xxx:8081/api/app/checkServerStatus"  # 临时服务健康检查
NGINX_CONF="/root/nginx/conf.d/default.conf"  # Nginx配置文件路径
MAX_RETRY=30  # 最大重试次数
RETRY_INTERVAL=5  # 重试间隔(秒)
# =========================================================

# 后台监控公网接口状态
start_monitor() {
    echo -e "\033[36m=== 开始实时监控公网接口:${MONITOR_URL} ===\033[0m"
    echo -e "\033[36m=============================================\033[0m"
    
    total=0
    failed=0
    
    while true; do
        current_time=$(date "+%Y-%m-%d %H:%M:%S.%3N")
        HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --insecure ${MONITOR_URL} 2>/dev/null)
        total=$((total+1))
        
        if [ "$HTTP_CODE" = "200" ]; then
            echo -e "\033[32m[${current_time}] 公网接口访问成功 | 状态码:${HTTP_CODE}\033[0m"
        else
            failed=$((failed+1))
            echo -e "\033[31m[${current_time}] 公网接口访问失败 | 状态码:${HTTP_CODE}\033[0m"
        fi
        sleep 1
    done
}

# 检查端口接口是否就绪
check_port_ready() {
    local url=$1
    local desc=$2
    echo -e "\033[36m=== 等待${desc}就绪(独立端口:${url})===\033[0m"
    local retry=0
    while [ $retry -lt $MAX_RETRY ]; do
        HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --insecure ${url} 2>/dev/null)
        if [ "$HTTP_CODE" = "200" ]; then
            echo -e "\033[32m✅ ${desc}就绪!(状态码:200)\033[0m"
            return 0
        fi
        retry=$((retry+1))
        echo -e "\033[33m⚠️  重试${retry}/${MAX_RETRY}:${desc}端口返回${HTTP_CODE},等待${RETRY_INTERVAL}秒...\033[0m"
        sleep ${RETRY_INTERVAL}
    done
    echo -e "\033[31m❌ 错误:${desc}启动超时(未返回200)\033[0m"
    kill $MONITOR_PID >/dev/null 2>&1
    exit 1
}

# 切换Nginx代理地址
switch_nginx_proxy() {
    local target=$1
    local desc=$2
    echo -e "\033[36m\n=== 切换Nginx代理到${desc} ===\033[0m"
    if [ "$target" = "temp" ]; then
        sed -i 's|proxy_pass http://app-servers|proxy_pass http://app-temp:8080|g' ${NGINX_CONF}
    else
        sed -i 's|proxy_pass http://app-temp:8080|proxy_pass http://app-servers|g' ${NGINX_CONF}
    fi
    docker exec -it nginx nginx -s reload >/dev/null 2>&1
    if [ $? -eq 0 ]; then
        echo -e "\033[32m✅ Nginx代理已切换到${desc}!\033[0m"
    else
        echo -e "\033[33m⚠️  Nginx重载警告,手动执行:docker exec -it nginx nginx -s reload\033[0m"
    fi
}

# ===================== 主流程 =====================
# 1. 启动后台监控
start_monitor &
MONITOR_PID=$!

# 2. 启动临时副本
echo -e "\033[36m\n1. 启动临时业务副本(8081端口)\033[0m"
docker compose up -d app-temp

# 3. 检查临时副本就绪
if ! check_port_ready "${TEMP_CHECK_URL}" "临时副本"; then
    kill $MONITOR_PID >/dev/null 2>&1
    exit 1
fi

# 4. 切换Nginx代理到临时副本
switch_nginx_proxy "temp" "临时副本"

# 5. 重启主副本
echo -e "\033[36m\n3. 重启主业务副本(8080端口)\033[0m"
docker compose up -d --no-deps --force-recreate app-main

# 6. 检查主副本就绪
if ! check_port_ready "${MAIN_CHECK_URL}" "主副本"; then
    echo -e "\033[31m\n⚠️  警告:主副本启动超时,临时副本仍在提供服务\033[0m"
    echo -e "\033[31m手动关闭命令:docker compose stop app-temp && docker compose rm -f app-temp\033[0m"
    kill $MONITOR_PID >/dev/null 2>&1
    exit 2
fi

# 7. 切换Nginx代理回主副本
switch_nginx_proxy "main" "主副本"

# 8. 关闭临时副本
echo -e "\033[36m\n5. 关闭临时副本\033[0m"
docker compose stop app-temp
docker compose rm -f app-temp

# 9. 停止监控并输出结果
kill $MONITOR_PID >/dev/null 2>&1
sleep 1

echo -e "\033[32m\n=============================================\033[0m"
echo -e "\033[32m✅ 发布完成!零停机验证结果:\033[0m"
echo -e "\033[32m   - 公网接口全程无502/中断\033[0m"
echo -e "\033[32m   - 文件访问:https://your-domain.com/api/profile/upload/test.bin 正常\033[0m"
echo -e "\033[32m=============================================\033[0m"

exit 0

三、替换说明(必看)

四、404 问题解决核心逻辑(关键)

  1. 文件目录挂载:Nginx 容器和业务容器挂载同一个宿主机文件目录,解决 Docker 隔离导致的文件不可见;
  2. 路径重写:通过rewrite ^/api/profile/upload/(.*)$ /profile/upload/$1break去掉/api前缀,匹配业务服务原生文件路径;
  3. 代理优先:放弃 Nginx静态访问文件,统一代理到业务服务,复用业务的文件访问逻辑;
  4. 流量同步切换:发布脚本切换 Nginx代理地址时,文件访问请求会同步切换到临时副本,避免发布中断。

五、使用步骤

  1. 复制上述 3 个文件到服务器对应目录;
  2. 全局替换所有占位符为自身环境值;
  3. 赋予发布脚本执行权限:chmod +x deploy_app.sh;
  4. 启动基础服务:docker compose up -d redis nginx app-main;
  5. 验证文件访问:curl https://your-domain.com/api/profile/upload/test.bin;
  6. 零停机发布:./deploy_app.sh。

六、验证 404 问题是否解决

# 1. 验证域名访问文件
curl -I https://your-domain.com/api/profile/upload/test.bin

# 2. 验证发布过程中文件访问不中断
./deploy_app.sh  # 执行发布脚本,同时另起终端持续curl文件地址

返回状态码200即表示 404 问题解决。

总结

  1. 核心解决 Docker 容器文件隔离问题:Nginx 和业务容器挂载相同宿主机文件目录;
  2. 统一路径规则:通过 Nginx重写去掉/api前缀,匹配业务服务原生文件路径;
  3. 发布时流量同步切换:文件访问请求随 Nginx 代理地址切换到临时副本,避免 404/ 中断。

到此这篇关于Docker+Nginx单副本零停机发布实现并解决文件访问 404的文章就介绍到这了,更多相关Docker Nginx单副本零停机发布内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Docker 使用国内镜像仓库的方法

    Docker 使用国内镜像仓库的方法

    这篇文章主要介绍了Docker 使用国内镜像仓库的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • supervisor下的Dockerfile的多服务镜像封装操作

    supervisor下的Dockerfile的多服务镜像封装操作

    这篇文章主要介绍了supervisor下的Dockerfile的多服务镜像封装操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • Docker资源(CPU/内存/磁盘IO)限制全解析

    Docker资源(CPU/内存/磁盘IO)限制全解析

    在Docker容器化部署中,资源隔离与限制是保障系统稳定性的关键,本文将从CPU、内存、磁盘IO三大核心资源入手,详细讲解Docker资源限制的参数配置、实操验证、注意事项,同时补充空间清理、命令速查与避坑指南
    2025-12-12
  • 常用Docker命令总结大全

    常用Docker命令总结大全

    这篇文章主要介绍了常用Docker命令总结大全的相关资料,需要的朋友可以参考下
    2026-01-01
  • Docker拉取容器镜像超时的问题解决办法

    Docker拉取容器镜像超时的问题解决办法

    这篇文章主要介绍了Docker拉取容器镜像超时问题的解决办法,解决方法包括配置Docker镜像加速器、设置代理、通过中介设备传送镜像等,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2025-02-02
  • Docker安全开放远程访问连接权限方式

    Docker安全开放远程访问连接权限方式

    文章介绍了如何配置Docker以实现远程访问、开启认证和通信加密,包括生成证书和私钥、配置Docker守护进程以及在IDEA和Maven中连接Docker服务的方法
    2024-11-11
  • 使用Docker部署Nacos并配置MySQL数据源的详细步骤

    使用Docker部署Nacos并配置MySQL数据源的详细步骤

    Nacos是阿里巴巴开源的服务发现、配置管理和服务管理平台,它提供了注册中心和配置中心的功能,能够轻松地管理微服务的注册与发现,以及动态配置的管理,这篇文章主要给大家介绍了关于使用Docker部署Nacos并配置MySQL数据源的超详细步骤,需要的朋友可以参考下
    2024-05-05
  • 树莓派4b ubuntu19 server 安装docker-ce的安装步骤

    树莓派4b ubuntu19 server 安装docker-ce的安装步骤

    这篇文章主要介绍了树莓派4b ubuntu19 server 安装docker-ce的安装步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • docker容器内部可以访问,外部无法访问的处理

    docker容器内部可以访问,外部无法访问的处理

    这篇文章主要介绍了docker容器内部可以访问,外部无法访问的处理方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • dockerfile基于apline将JDK20打包成镜像

    dockerfile基于apline将JDK20打包成镜像

    这篇文章主要为大家介绍了dockerfile基于apline将JDK20打包成镜像步骤及验证,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-02-02

最新评论