You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Docker化Next.js13+Go应用登录遇服务端异常及ECONNREFUSED错误

问题:Docker化Next.js+Go应用后登录功能异常
  • Firefox浏览器显示服务端异常,控制台出现XHRGET http://127.0.0.1:3000/logout?_rsc=acgkz 404错误;
  • Docker容器日志显示前端请求http://127.0.0.1:8080时触发connect ECONNREFUSED 127.0.0.1:8080错误;
  • 本地非Docker环境登录正常,Docker环境下前端http://127.0.0.1:3000/login和后端http://127.0.0.1:8080均可访问,且前端容器能ping通后端容器,但登录时无法建立连接。

Docker容器日志

2024-01-08 08:41:03 backend   | 2024/01/08 06:41:03 INFO loaded config file=config/config.dev.json
2024-01-08 08:41:03 backend   | Server running on http://127.0.0.1:8080
2024-01-08 08:41:04 frontend  |   ▲ Next.js 13.5.4
2024-01-08 08:41:04 frontend  |   - Local:        http://localhost:3000
2024-01-08 08:41:04 frontend  |   - Network:      http://0.0.0.0:3000
2024-01-08 08:41:04 frontend  | 
2024-01-08 08:41:04 frontend  |  ✓ Ready in 63ms
2024-01-08 08:42:15 frontend  | TypeError: fetch failed
2024-01-08 08:42:15 frontend  |     at Object.fetch (node:internal/deps/undici/undici:11730:11)
2024-01-08 08:42:15 frontend  |     at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
2024-01-08 08:42:15 frontend  |   cause: Error: connect ECONNREFUSED 127.0.0.1:8080
2024-01-08 08:42:15 frontend  |       at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16)
2024-01-08 08:42:15 frontend  |       at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:128:17) {
2024-01-08 08:42:15 frontend  |     errno: -111,
2024-01-08 08:42:15 frontend  |     code: 'ECONNREFUSED',
2024-01-08 08:42:15 frontend  |     syscall: 'connect',
2024-01-08 08:42:15 frontend  |     address: '127.0.0.1',
2024-01-08 08:42:15 frontend  |     port: 8080
2024-01-08 08:42:15 frontend  |   }
2024-01-08 08:42:15 frontend  | }

docker compose.yaml

version: '3.9'

services:
  frontend:
    container_name: frontend
    image: frontend
    build:
      context: ./frontend
      dockerfile: frontend.dockerfile
    ports:
      - '3000:3000'
    restart: always
    depends_on:
      - backend
    networks:
      - social-network

  backend:
    container_name: backend
    image: backend
    build:
      context: .
      dockerfile: backend/backend.dockerfile
    ports:
      - "8080:8080"
    networks:
      - social-network

networks:
        social-network:

frontend.dockerfile

FROM node:18-alpine AS base
FROM base AS deps

RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


FROM base AS builder
WORKDIR /app

ENV BUILD_STANDALONE true

COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN yarn build

FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

RUN apk add --no-cache curl
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/.next ./_next
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

backend.dockerfile

FROM golang:latest
WORKDIR /app
COPY backend/go.mod backend/go.sum ./
RUN go mod download
RUN apt-get update && apt-get install -y curl
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
COPY backend/ .
RUN go build -o main ./server.go
EXPOSE 8080
CMD ["./main"]

Login.tsx

"use client"

import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
import { isPasswordValid, isUsernameOrEmailValid } from "./validation";

export default function Login() {

    const [userId, setUserId] = useState('');
    const [password, setPassword] = useState('');
    const [usernameOrEmailError, setUsernameOrEmailError] = useState('');
    const [passwordError, setPasswordError] = useState('');
    const router = useRouter()

    const [isLoading, setIsLoading] = useState(false);
    const [canSubmit, setCanSubmit] = useState(true);

    async function onSubmit(event: FormEvent<HTMLFormElement>) {
        event.preventDefault()
        setIsLoading(true)
        let canSubmitForm = true;
        try {
            setIsLoading(true);
            const userIdValid = isUsernameOrEmailValid(userId);
            if (!userIdValid) {
                setUsernameOrEmailError("Invalid username");
                setCanSubmit(false);
                canSubmitForm = false;
                return;
            }
            setUsernameOrEmailError("");
            const passwordValid = isPasswordValid(password);
            if (!passwordValid) {
                setPasswordError("Invalid password");
                setCanSubmit(false);
                canSubmitForm = false;
                return;
            }
            setPasswordError("");
            if (canSubmit || canSubmitForm) {
                const formData = {
                    userId,
                    password
                }
                setUserId('');
                setPassword('');

                const response = await fetch("http://127.0.0.1:8080/api/v1/users/login", {
                    method: 'POST',
                    body: JSON.stringify(formData),
                    mode: "cors",
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    credentials: 'include',
                })
                if (!response.ok) {
                    return response.text().then(errorMessage => {
                        console.error(`Error: ${response.status}`);
                        console.error(errorMessage);
                    });
                }
            } else {
                return;
            }
        } catch (error) {
            console.error('Error during login:', error);
        } finally {
            setIsLoading(false);
            if (canSubmitForm) {
                router.refresh();
            }
        }

    }

    return (<> .... .... .... </>)

}

解决方案

1. 修正Go后端监听地址

日志显示后端服务绑定在127.0.0.1:8080,这意味着服务仅能在容器内部的回环地址访问,前端容器无法通过Docker网络连接到它。需要修改Go服务器代码,将监听地址改为0.0.0.0:8080,这样服务会绑定到容器的所有网络接口,允许Docker网络中的其他容器访问。

示例修改(server.go):

// 原代码
log.Fatal(http.ListenAndServe("127.0.0.1:8080", router))
// 修改为
log.Fatal(http.ListenAndServe("0.0.0.0:8080", router))

2. 修正前端API请求地址

前端代码中直接写死了http://127.0.0.1:8080,在Docker环境下,前端容器的127.0.0.1指向自身,而非后端容器。需要将请求地址改为Docker Compose服务名backend(对应compose文件中的服务名称),Docker会自动通过内部网络解析到后端容器。

修改Login.tsx中的fetch地址:

// 原代码
const response = await fetch("http://127.0.0.1:8080/api/v1/users/login", { ... })
// 修改为
const response = await fetch("http://backend:8080/api/v1/users/login", { ... })

额外优化:使用环境变量管理API地址

为了适配不同环境(本地/Docker),建议将API地址通过环境变量配置:

  1. 在前端项目的.env文件中添加:
# 本地环境
NEXT_PUBLIC_API_URL=http://127.0.0.1:8080
  1. 在docker-compose.yaml的frontend服务中添加环境变量:
services:
  frontend:
    # ... 其他配置
    environment:
      - NEXT_PUBLIC_API_URL=http://backend:8080
  1. 修改Login.tsx中的fetch代码:
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/users/login`, { ... })

内容的提问来源于stack exchange,提问作者Egon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 15:35:53