Next.js: Deploying static files to AWS S3 (with Cloudfront)

Created on 25 Jan 2018  路  11Comments  路  Source: vercel/next.js

Hey guys, just read few posts on copying static files to S3 CDN. Idea is to copy static files to AWS S3 after the build, so CDN will pick them up with assetPrefix without even hitting the production sever (which only role is to do SSR, without serving the static assets). I generally made it to work but this is kind of cumbersome solution:

(... install awscli and jq)
- NEXT_BUILD_ID=$( cat ./.next/BUILD_ID )
- aws s3 cp ./.next/bundles/pages s3://BUCKET_NAME/_next/${NEXT_BUILD_ID}/page --recursive --acl "public-read"
- NEXT_APP_ID=$( cat ./.next/build-stats.json | jq '."app.js".hash' -r )
- aws s3 cp ./.next s3://BUCKET_NAME/_next/${NEXT_APP_ID} --recursive --exclude "*dist/*" --exclude "*bundles/*" --acl "public-read"

To explain: as far as I understand, static bundles are in .next/bundles folder, and they are prefixed with BUILD_ID, but app.js and manifest and other files are in root of the .next dir and prefixed with hash from build-stats.json.

So, in short: NEXT_BUILD_ID is prefix for static assets, and NEXT_APP_ID is prefix for app.js, manifest and other root files (and you need jq to extract the hash from json file).

This works, however, things get even more messy since I am building a Docker container, so I need to upload those files to S3 directly from the container, after the build is done.

One proposal to solution would be to have consistent BUILD_ID, when you run consequent builds without file changes (as in build-stats.json). So you can run build on some CI server, then build the container (and BUILD_ID will be in sync, since files are not changed).

Is there any more elegant solution for this problem? Just to remind, I don't want to bother server with static files at all.

UPDATE: Here's my final solution (if there is not anything better) for guys who use AWS ECS, and want cloudfront:

  1. Build next in production mode
  2. Copy files to S3 as explained above
  3. When building docker container just COPY files and run yarn install, don't run next production build in docker container

This way you save some time by not running next build in the container, and the versioning will be in sync. If you have any questions feel free to ask!

Thanks in advance!

Most helpful comment

Hey all - I wrote this up what I did to get it working with S3 + cloudfront + server-side-rendering at compile time here: https://gist.github.com/rbalicki2/30e8ee5fb5bc2018923a06c5ea5e3ea5

All 11 comments

@radenkovic This is a great question. I have been asking the same one myself some weak ago. I think that you have the opportunity to improve this solution in three different aspects:

  1. Use a multi-stage Dockerfile. Your Next.js server doesn't need the client bundle to run nor the dev dependencies. You can make the docker image lighter by pushing the assets to S3 in a multi-stage Dockerfile.
  2. Set the cache control headers. Cloudfront won't do it for you. You can set it to immutable.
  3. Use the export feature of Next.js. This will make the process much simpler. No need to know the internal of the .next folder.

I use something iike this:

FROM node:9.4.0
RUN apt-get update && apt-get install -y python3-pip && pip3 install awscli --upgrade --user
COPY ./app /app
ARG AWS_REGION
ENV NODE_ENV production
WORKDIR /app
RUN yarn \
    && yarn next build \
    && yarn next export -o .next-export \
    && rm -rf .next/bundles \
    && ~/.local/bin/aws s3 mv --recursive --cache-control='public, max-age=31536000, immutable' \
       /app/.next-export/_next s3://BUCKET_NAME/_next

FROM node:9.4.0
RUN apt-get update && ...
COPY ./docker/node/remote/bin/* /usr/bin/
RUN chmod u+x /usr/bin/start.sh
COPY --from=0 /app /app
ENV NODE_ENV production
WORKDIR /app
ENTRYPOINT ["/usr/bin/start.sh"]

@oliviertassinari Hey, thanks for the input! Added cache control recently, that's great. However, I am not sure if next export would achieve the same effect: basically next export will create completely static website (without need for server), which is not what I want. I want to keep SSR part intact, and offload static file serving. I have a lot of dynamic stuff that happens on server (initial render, apollo server tree rundown) and many pages (eg every user registered has public facing server-side page, with data fetching), so static site won't be feasible.

Since I am building on CI tool, i am already doing multi-stage docker:

  1. in the CI docker environment
  2. In the docker container that is built inside CI 馃拑

basically next export will create completely static website (without need for server), which is not what I want.

@radenkovic This is what I'm doing with the documentation website of Material-UI, it's a static website hosted on Firebase (easy of deployment) + Cloudflare (scaling). It's how I had the idea.

No, the Dockerfile example I have provided has nothing to do with a static website. It's for dynamic website hosted on AWS + Cloudfront. The key is to use the export feature of Next.js to get the .js files in a clean state, without having to reverse engineer the .next structure.

next.config.js:

  // It's needed for generating the Next.js bundle assets with `yarn next export`.
  exportPathMap: () => {
    return {}
  },
  assetPrefix: config.get('web.assetUrl'),
  poweredByHeader: false,
}

Thanks!!! Sounds great, will try to set it up during the weekend and maybe we can propose docs update, this is important stuff for AWS/docker users!

@radenkovic What CI are you using for building the docker images?

@oliviertassinari GitlabCI but you can do it on travis too, just use image:docker

I have next + cloudfront + s3 working on multiple projects. Please, feel free to tweet to me @ statisticsftw and I can help you get it all set up... at some point in the future, I'll write a blog post about it, but in the mean time, happy to help :)

Hey all - I wrote this up what I did to get it working with S3 + cloudfront + server-side-rendering at compile time here: https://gist.github.com/rbalicki2/30e8ee5fb5bc2018923a06c5ea5e3ea5

I have successfully deployed a dynamic React app to S3 through this guide: https://www.fullstackreact.com/articles/deploying-a-react-app-to-s3/

Is there not something equivalent you can do with Next?

@dotkas nextjs is basically nodejs express server, so you need something like ec2 to run it. Other solution is to explore export static next app and upload it to s3. I think you are confused what "dynamic" means.

Hi, I have a legacy system that I'm trying to port to nextjs. The earlier system did the following using gulp tasks:

  • look at each file in a single /dist folder

    • which I want to now work on both /.next and /static

  • figure out if it has been updated or not
  • for the updated ones create a manifest of new file names with unique ids
  • upload those files on s3 and manage cloudfront with the unique names
  • write a manifest file json on the file system
  • the server will then use that manifest with a helper function to require filenames
  • with this approach we

    • didn't have to upload hundreds of files every time, only the updated ones

    • kept an archive of all the old files

Sharing below portions of code and output to better illustrate the process. Here is the portion of the gulpfile that did the work

const RevAll = require('gulp-rev-all');
const awspublish = require('gulp-awspublish');
const cloudfront = require('gulp-cloudfront');

const deployToCDN = () => {
  const awsCreds = JSON.parse(
    fs.readFileSync('app/config/aws-creds.json', 'utf8')
  );
  const headers = { 'Cache-Control': 'max-age=86400, no-transform, public' };
  const publisher = awspublish.create(awsCreds);

  return gulp
    .src('./app/public/dist/**/*')
    .pipe(
      rename(dirpath => {
        dirpath.dirname = 'dist/' + dirpath.dirname;
      })
    )
    .pipe(
      RevAll.revision({
        includeFilesInManifest: ['.js', '.css', '.png', '.jpg', '.ico', '.wav']
      })
    )
    .pipe(awspublish.gzip())
    .pipe(publisher.publish(headers))
    .pipe(publisher.cache())
    .pipe(awspublish.reporter())
    .pipe(cloudfront(awsCreds))
    .pipe(RevAll.manifestFile())
    .pipe(gulp.dest('./app/config'));
};

Here is sample portion of the manifest file it generated

{
  "dist/css/desk.lp.1.min.css": "dist/css/desk.lp.1.min.a134392a.css",
  "dist/css/minimal.min.css": "dist/css/minimal.min.c68d5e57.css",
  "dist/css/mob.lp.1.min.css": "dist/css/mob.lp.1.min.c442674c.css",
  "dist/images/avatar.png": "dist/images/avatar.1a061ccf.png"
}

Here is the helper function server used

const manifestPath = JSON.parse(
  fs.readFileSync('app/config/rev-manifest.json', 'utf8')
);
export const asset = env => {
  return assetPath => {
    const cdnBase = '//llalalalalala.cloudfront.net';
    if (env === 'production') {
      const cdnPath = `${cdnBase}/${
        manifestPath[`/dist/${assetPath}`.slice(1)]
      }`;
      return cdnPath;
    }
    return assetPath;
  };
};

// sample usage inside a server rendered pug file
asset('/images/avatar.png')

I am hoping to recreate the same functionality in nextjs now. As you can see it deals with more than just the upload of files on s3. Can anyone guide me to a possible solution?

Was this page helpful?
0 / 5 - 0 ratings