Next.js 从 Docker 容器无限重新加载

Posted

技术标签:

【中文标题】Next.js 从 Docker 容器无限重新加载【英文标题】:Next.js infinite reload from Docker container 【发布时间】:2019-09-17 00:11:46 【问题描述】:

我正在尝试制作一个简单的 Next.js 应用,它使用 Firebase 身份验证并从 Docker 容器运行。

以下在本地运行良好(从构建的 docker 容器运行)。但是,当我部署到 Heroku 或 Google Cloud Run 并访问网站时,它会导致无限的重新加载循环(页面只是冻结并最终耗尽内存。当作为 Node.js 应用程序从 Google 提供时它工作正常应用引擎。

我认为错误在 Dockerfile 中(我认为我在端口上做错了)。 Heroku 和 Google Cloud Run 随机化他们的 process.env.PORT 环境变量,如果这有任何用处,并且据我所知忽略 Docker 的 EXPOSE 命令。

重新加载时,网络/控制台中不会显示任何错误。我认为这是由于 Next.js 8 的热模块重新加载,但问题在 Next.js 7 上仍然存在。

相关文件如下。

Dockerfile

FROM node:10

WORKDIR /usr/src/app

COPY package*.json ./
RUN yarn

# Copy source files.
COPY . .

# Build app.
RUN yarn build

# Run app.
CMD [ "yarn", "start" ]

server.js

require(`dotenv`).config();

const express = require(`express`);
const bodyParser = require(`body-parser`);
const session = require(`express-session`);
const FileStore = require(`session-file-store`)(session);
const next = require(`next`);
const admin = require(`firebase-admin`);
const  serverCreds  = require(`./firebaseCreds`);

const COOKIE_MAX_AGE = 604800000; // One week.

const port = process.env.PORT;
const dev = process.env.NODE_ENV !== `production`;
const secret = process.env.SECRET;

const app = next( dev );
const handle = app.getRequestHandler();

const firebase = admin.initializeApp(
  
    credential: admin.credential.cert(serverCreds),
    databaseURL: process.env.FIREBASE_DATABASE_URL,
  ,
  `server`,
);

app.prepare().then(() => 
  const server = express();

  server.use(bodyParser.json());
  server.use(
    session(
      secret,
      saveUninitialized: true,
      store: new FileStore( path: `/tmp/sessions`, secret ),
      resave: false,
      rolling: true,
      httpOnly: true,
      cookie:  maxAge: COOKIE_MAX_AGE ,
    ),
  );

  server.use((req, res, next) => 
    req.firebaseServer = firebase;
    next();
  );

  server.post(`/api/login`, (req, res) => 
    if (!req.body) return res.sendStatus(400);

    const  token  = req.body;
    firebase
      .auth()
      .verifyIdToken(token)
      .then((decodedToken) => 
        req.session.decodedToken = decodedToken;
        return decodedToken;
      )
      .then(decodedToken => res.json( status: true, decodedToken ))
      .catch(error => res.json( error ));
  );

  server.post(`/api/logout`, (req, res) => 
    req.session.decodedToken = null;
    res.json( status: true );
  );

  server.get(`/profile`, (req, res) => 
    const actualPage = `/profile`;
    const queryParams =  surname: req.query.surname ;
    app.render(req, res, actualPage, queryParams);
  );

  server.get(`*`, (req, res) => handle(req, res));

  server.listen(port, (err) => 
    if (err) throw err;
    console.log(`Server running on port: $port`);
  );
);

_app.js

import React from "react";
import App,  Container  from "next/app";
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import "isomorphic-unfetch";
import  clientCreds  from "../firebaseCreds";
import  UserContext  from "../context/user";
import  login, logout  from "../api/auth";

const login = ( user ) => user.getIdToken().then(token => fetch(`/api/login`, 
  method: `POST`,
  headers: new Headers( "Content-Type": `application/json` ),
  credentials: `same-origin`,
  body: JSON.stringify( token ),
));

const logout = () => fetch(`/api/logout`, 
  method: `POST`,
  credentials: `same-origin`,
);

class MyApp extends App 
  static async getInitialProps( ctx, Component ) 
    // Get Firebase User from the request if it exists.
    const user = getUserFromCtx( ctx );
    const pageProps = Component.getInitialProps ? await Component.getInitialProps( ctx ) : ;
    return  user, pageProps ;
  

  constructor(props) 
    super(props);
    const  user  = props;
    this.state = 
      user,
    ;

    if (firebase.apps.length === 0) 
      firebase.initializeApp(clientCreds);
    
  

  componentDidMount() 
    firebase.auth().onAuthStateChanged((user) => 
      if (user) 
        login( user );
        return this.setState( user );
      
    );
  

  doLogin = () => 
    firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider());
  ;

  doLogout = () => 
    firebase
      .auth()
      .signOut()
      .then(() => 
        logout();
        return this.setState( user: null );
      );
  ;

  render() 
    const  Component, pageProps  = this.props;

    return (
      <Container>
        <UserContext.Provider
          value=
            user: this.state.user,
            login: this.doLogin,
            logout: this.doLogout,
            userLoading: this.userLoading,
          
        >
          <Component ...pageProps />
        </UserContext.Provider>
      </Container>
    );
  


export default MyApp;

更新:

可重现的回购代码是here。

说明在自述文件中,并且在本地可以正常工作。

【问题讨论】:

你试过本地NODE_ENV制作吗? 是的,工作正常! 已经解决了吗? @Towkir 不,仍然是个问题。 你用来在本地测试它的“docker run”命令是什么?根据您的 Dockerfile,即使在本地也不应该工作,因为容器内部没有 EXPOSED 端口。还有你用来测试它的 URL 是什么? 【参考方案1】:

硬编码服务器环境变量(而不是从 Heroku / Cloud Run 读取它们)解决了这个问题。

原因似乎是因为 Heroku / Cloud Run 上的环境变量在运行时可用,但在构建时不可用,因此 Docker 环境(和 server.js)无法从 process.env 访问它们。 Google App Engine here 也存在类似问题。

此解决方案并不理想,因为您可能必须将config/staging.js 保留在版本控制中,它会导致不同分支之间的合并冲突,但该冲突应该只发生一次。

server.js

const  envType  = require(`./utils/envType`);
const envPath = `./config/$envType.js`; // e.g. config/staging.js with env variables
const  env  = require(envPath);
...
const  envType  = require(`./utils/envType`);
const envPath = `./config/$envType.js`;
const  env  = require(envPath);

const nextConfig = 
  env:  ...env ,
;

module.exports = nextConfig;

【讨论】:

以上是关于Next.js 从 Docker 容器无限重新加载的主要内容,如果未能解决你的问题,请参考以下文章

TypeError: Object(...) is not a function 当将卷从本地目录挂载到 next.js 容器中时

为啥使用 Docker 容器的 NextJS 在更改开发环境的代码后没有重新加载?

NEXT JS - 如何防止重新安装布局?

Next.js 应用在生产环境中频繁重新加载

重新启动 Nginx 或在证书更改时重新加载证书缓存

监听更改并在代码更改时重新加载容器 - docker-compose