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:
yarn install
, don't run next production build in docker containerThis 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!
@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:
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:
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:
/dist
folder/.next
and /static
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?
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