Deploy a commercial Next.js application with pkg and docker

Michael Hsu
4 min readMay 14, 2018

--

As I mentioned in the “Make Your React Service Portal”, there is a scenario that we deliver commercial applications with docker. Letting customers operate their own applications without source code is the key point in this deploy flow.

Nowadays, Golang may be the best solution of back-end application and end up with the smallest binary file. Node.js is a common choice in front-end/React ecosystems. However, there is no built-in function to package Node.js project. Fortunately, the pkg CLI made by zeit does a great job for Node.js platform.

In this artical, we will introduce pkg to package the Next.js application into an single executable file and use Docker to wrap it with Alpine Linux environment.

PKG

The pkg CLI enables you to package your Node.js project into an executable that can be run even on devices without Node.js installed. — zeit/pkg

First, we need to describe pkg configuration of a Node.js project in package.json:

  1. bin: The entrypoint of Node.js project. You will need a custom server for the Next.js project.
  2. pkg.assets: Raw contents of the project. You might want to specify several public static files built by Next.js here.
  3. pkg.scripts: The JavaScript files need to be compiled into binary without sources.
pkg configuration in package.json [source]

Next.js file path problem

Second, the pkg puts packaged files in their own snapshot filesystem prefixed with /snapshot/ during the process. You may encounter some path related problems at runtime, we need to customize the next server of Next.js:

// server.jsconst next = require('next');
const nextConfig = require('./next.config');
const app = next({ dev, dir: __dirname, conf: nextConfig });

Then, run the command to package the entire application into an executable. You can also specify the target machine environments with --targets option and output path via --out-path as well.

$ pkg . --targets node9-alpine-x64 --out-path pkg# output to pkg/binary

Trick of reducing binary size for Next.js project

The original binary file is 85.4MB. The main bottleneck is that pkg package unnecessary files.

One is document-like files. We can introduce .yarnclean to remove them from node_modules (-0.2MB). The other is that Next.js treat devDependencies as dependencies. However, we don’t really need them for production usage (e.g. webpack):

$ yarn autoclean --force
$ rm -rf node_modules/webpack node_modules/webpack-dev-middleware node_modules/webpack-hot-middleware

The final size of binary file is about 66MB.

Docker multi-stage builds

Docker could be a popular solution to deliver the application with machine environments. Customers can directly use docker-compose or Kubernetes to manage container services.

To make the docker image even smaller, we use Docker multi-stage builds (requiring Docker 17.05 or higher) to reduce layers and remove useless files. Here, we copy the single binary artifact made by pkg from first stage to the second stage.

In the first stage named as “builder” stage, it cotains a regular npm flow. We use mhart/alpine-node as base image and copy all source codes into the container. Do the processes of installing node_modules, building Next.js and packaging into binary:

// Dockerfile - stage 1FROM mhart/alpine-node AS builder
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn run build
RUN yarn run pkg

In the second stage, we copy one single executable file from the first builder stage. We adopt the Alpine image alpine which is the smallest Linux image. Just to remind, you might need to install some missing apk libraries (e.g. libstdc++):

// Dockerfile - stage 2FROM alpine
RUN apk update && \
apk add --no-cache libstdc++ libgcc ca-certificates && \
rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=builder /app/pkg .
CMD ./next-app

Finally, build the docker image and run with predefined environment variables:

$ docker build -t app .
$ docker run --rm -it -p 3003:3003 -e "PORT=3003" app

Deploy to Now.sh

We now deliver the docker image to customers without source code. It is also possible to verify it by deploying the docker service to any PaaS. Now.sh supports Docker multi-stage builds in OSS plan. All we need is one command:

$ now --docker

That’s it. Awesome zeit! https://nextjs-pkg-docker-alpine.now.sh

Conclusion

In this article we take a look at an example of delivering a commercial Next.js application with pkg and docker. It is quite convenient to deploy it to Now.sh cloud service. Technically, this approach should work with every Node.js project.

The full source code is available on GitHub evenchange4/nextjs-pkg-docker-alpine repository.

Further Readings

  1. Make Your React Service Portable
  2. Microservice 產品交付
  3. 2018 GraphQL 漸進式導入的架構
  4. 五分鐘 Kubernetes 有感

References

  1. https://github.com/zeit/next.js/blob/canary/examples/with-pkg/README.md
  2. https://github.com/zeit/next.js/blob/canary/examples/with-docker/README.md

Thanks to Lynn Lin.

--

--