Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploying static files to AWS S3 (with Cloudfront) #3621

Closed
radenkovic opened this issue Jan 25, 2018 · 11 comments

Comments

@radenkovic
Copy link

commented Jan 25, 2018

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!

@oliviertassinari

This comment has been minimized.

Copy link
Contributor

commented Jan 31, 2018

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

This comment has been minimized.

Copy link
Author

commented Jan 31, 2018

@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 💃
@oliviertassinari

This comment has been minimized.

Copy link
Contributor

commented Jan 31, 2018

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,
}
@radenkovic

This comment has been minimized.

Copy link
Author

commented Jan 31, 2018

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!

@oliviertassinari

This comment has been minimized.

Copy link
Contributor

commented Jan 31, 2018

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

@radenkovic

This comment has been minimized.

Copy link
Author

commented Jan 31, 2018

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

@rbalicki2

This comment has been minimized.

Copy link

commented Mar 20, 2018

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 :)

@rbalicki2

This comment has been minimized.

Copy link

commented Apr 21, 2018

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

@dotkas

This comment has been minimized.

Copy link

commented Feb 6, 2019

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?

@radenkovic

This comment has been minimized.

Copy link
Author

commented Feb 6, 2019

@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.

@subodhpareek18

This comment has been minimized.

Copy link

commented Mar 27, 2019

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.