Next.js 从 Docker 容器无限重载

IT技术 javascript node.js reactjs docker
2021-05-04 07:46:09

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

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

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

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

相关文件如下。

文件

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" ]

服务器.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;

更新:

可重现的 repo 代码在这里

说明在 README 中,它在本地运行良好。

1个回答

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

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

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

服务器.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;