Next.js从Docker容器无限重新加载 [英] Next.js infinite reload from Docker container

查看:110
本文介绍了Next.js从Docker容器无限重新加载的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试创建一个简单的Next.js应用程序,该应用程序使用Firebase身份验证并从Docker容器运行。



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



我认为错误发生在Dockerfile中(我认为我的端口有问题).Heroku和Google Cloud Run将他们的<$ c随机化$ c> process.env.PORT 环境变量,如果有任何用处,并且根据我的意识忽略Docker的 EXPOSE 命令。 / p>

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



相关文件如下。



Dockerfile



  FROM节点:10 

WORKDIR / usr / src / app

COPY包* .json ./
RUN yarn

#复制源文件。
COPY ..

#构建应用程序
RUN纱线构建

#运行应用程序。
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(`se​​ssion-file-store`)(session);
const next = require(`next`);
const admin = require(`firebase-admin`);
const {serverCreds} = require(`。/ firebaseCreds`);

const COOKIE_MAX_AGE = 604800000; // 一周。

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(
{
凭证: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`,秘密}),
resave:false,
滚动: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 = decodingToken;
return decodingToken;
})
.then(decodingToken => res.json({status:true,decodingToken}))
。 catch(错误=> 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(`运行在端口上的服务器:$ {port} `);
});
});



_app.js



  import fromreact; 
从next / app导入App,{Container};
从firebase / app导入firebase;
importfirebase / auth;
导入firebase / firestore;
importisomorphic-unfetch;
从../firebaseCreds导入{clientCreds};来自../context/user的
import {UserContext};来自../api/auth的
import {login,logout};

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

const logout =()=> fetch(`/ api / logout`,{
method:`POST`,
凭证:`same-origin`,
});

类MyApp扩展App {
static async getInitialProps({ctx,Component}){
//从请求中获取Firebase用户(如果存在)。
const user = getUserFromCtx({ctx});
const pageProps = Component.getInitialProps?等待Component.getInitialProps({ctx}):{};
return {user,pageProps};
}

构造函数(道具){
super(道具);
const {user} =道具;
this.state = {
user,
};

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

componentDidMount(){
firebase.auth()。onAuthStateChanged((user)=> {
if(user){
登录({user});
返回this.setState({user});
}
});
}

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

doLogout =()=> {
firebase
.auth()
.signOut()
.then(()=> {
logout();
返回此信息。 setState({user:null});
});
};

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

return(
< Container>
< UserContext.Provider
value = {{
user:this.state.user,
登录:this.doLogin,
注销:this.doLogout,
userLoading:this.userLoading,
}}
>
< Component {... pageProps} />
< /UserContext.Provider>
< / Container>
);
}
}

导出默认MyApp;



更新:



可重复的回购代码是此处



说明在README中,它在本地工作正常。

解决方案

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



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



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



server.js



  const {envType} = require(` 。/ utils的/ envType`); 
const envPath =`./config / $ {envType} .js`; //例如带有env变量的config / staging.js
const {env} = require(envPath);
...



  const {envType} =要求(`/ utils的/ envType`); 
const envPath =`./config / $ {envType} .js`;
const {env} = require(envPath);

const nextConfig = {
env:{... env},
};

module.exports = nextConfig;


I'm trying to make a simple Next.js app which uses Firebase auth and runs from a Docker container.

The following works fine locally (running from a built docker container). However, when I deploy to Heroku or Google Cloud Run and go to the website, it results in an infinite reload loop (page just freezes up and eventually runs out of memory. It works fine when being served as a Node.js app from Google App Engine.

I think the error is in the Dockerfile (I think I'm doing something wrong with the ports). Heroku and Google Cloud Run randomize their process.env.PORT environment variable, if that's any use, and ignore Docker's EXPOSE commands as far as I'm aware.

No errors are shown in Network / Console when the reloads are happening. I thought it was due to Next.js 8's hot module reload, but the issue persists on Next.js 7 as well.

The relevant files are below.

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;

Update:

Reproducible repo code is here.

Instructions are in the README, and it works fine locally.

解决方案

Hardcoding the server environment variables (instead of reading them from Heroku / Cloud Run) solves this issue.

The reason for this seems to be because environment variables on Heroku / Cloud Run are available at runtime but not build time, so the Docker environment (and server.js) does not have access to them from process.env. There's a similar issue with Google App Engine here.

This solution is not ideal as you may have to keep config/staging.js in version control, and it will result in a merge conflict between different branches, but that conflict should only happen once.

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容器无限重新加载的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆