How to Deploy on AWS w/ S3 and Cloudfront

AWS is Amazon Web Services.
S3 is their static storage which can be configured for Static Site Hosting. Cloudfront is their CDN (content delivery network)

Hosting a static generated Nuxt app on AWS w/ S3 + Cloudfront is powerful and cheap.

AWS is a death by 1000 paper cuts. If we missed a step, please submit a PR to update this document.


We will use the Gulp build system for this. Gulp is a mature build system with handy utilities we can use.

Our deploy script needs these environment variables set:


Setting it up

  1. Make a S3 bucket and configure it for static site hosting
  2. Create a cloudfront distribution
  3. Configure security access
  4. Setup build script in your project

1. Setup your AWS S3 bucket and 2. Setup your Cloudfront Distribution

For steps 1. and 2, follow this tutorial to setup your S3 and Cloudfront

You should now have this data:


3. Configure security access

For step 3, we need to create a user that can:

  • Update the bucket contents
  • Invalidate the cloudfront distribution (propagates changes to users faster)

Create a programmatic user with this policy:

NOTE: replace 2x with your S3 bucket name below. This policy allows pushing to the specified bucket, and invalidating any cloudfront distribution.

    "Version": "2012-10-17",
    "Statement": [ {
            "Effect": "Allow",
            "Action": [ "s3:ListBucket" ],
            "Resource": [
        }, {
            "Effect": "Allow",
            "Action": [
            "Resource": [
        }, {
            "Effect": "Allow",
            "Action": [
            "Resource": "*"

Then get an access key and secret.

You should now have this data:


4. Setup your project's build script

4.1) Add Gulp to your project and to your command line

npm install --save-dev gulp gulp-awspublish gulp-cloudfront-invalidate-aws-publish concurrent-transform
npm install -g gulp

4.2) Create a script. See optional nvm (node version manager).


# Load nvm (node version manager), install node (version in .nvmrc), and npm install packages
[ -s "$HOME/.nvm/" ] && source "$HOME/.nvm/" && nvm use
# Npm install if not already.
[ ! -d "node_modules" ] && npm install

npm run generate

4.3) Make runnable and don't check into git

chmod +x
echo "
# Don't commit build files
" >> .gitignore

4.4) Create a gulpfile.js with the build script

var gulp = require('gulp');
var awspublish = require('gulp-awspublish');
var cloudfront = require('gulp-cloudfront-invalidate-aws-publish');
var parallelize = require('concurrent-transform');


var config = {

  // Required
  params: { Bucket: process.env.AWS_BUCKET_NAME },
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,

  // Optional
  deleteOldVersions: false,                 // NOT FOR PRODUCTION
  distribution: process.env.AWS_CLOUDFRONT, // Cloudfront distribution ID
  region: process.env.AWS_DEFAULT_REGION,
  headers: { /*'Cache-Control': 'max-age=315360000, no-transform, public',*/ },

  // Sensible Defaults - gitignore these Files and Dirs
  distDir: 'dist',
  indexRootPath: true,
  cacheFileName: '.awspublish',
  concurrentUploads: 10,
  wait: true,  // wait for Cloudfront invalidation to complete (about 30-60 seconds)

gulp.task('deploy', function() {
  // create a new publisher using S3 options
  var publisher = awspublish.create(config, config);

  var g = gulp.src('./' + config.distDir + '/**');
    // publisher will add Content-Length, Content-Type and headers specified above
    // If not specified it will set x-amz-acl to public-read by default
  g = g.pipe(parallelize(publisher.publish(config.headers), config.concurrentUploads))

  // Invalidate CDN
  if (config.distribution) {
    console.log('Configured with Cloudfront distribution');
    g = g.pipe(cloudfront(config));
  } else {
    console.log('No Cloudfront distribution configured - skipping CDN invalidation');

  // Delete removed files
  if (config.deleteOldVersions) g = g.pipe(publisher.sync());
  // create a cache file to speed up consecutive uploads
  g = g.pipe(publisher.cache());
  // print upload updates to console
  g = g.pipe(awspublish.reporter());
  return g;

4.5) Deploy and debug

Run it:


You should get an output similar to this:

$ ./                                                                                                                                                          Mod master

Found '/home/michael/scm/' with version <8>
Now using node v8.11.2 (npm v5.6.0)

> generate /home/michael/scm/
> nuxt generate

  nuxt:generate Generating... +0ms
  nuxt:build App root: /home/michael/scm/ +0ms
  nuxt:build Generating /home/michael/scm/ files... +0ms
  nuxt:build Generating files... +36ms
  nuxt:build Generating routes... +10ms
  nuxt:build Building files... +24ms
  ████████████████████ 100% 

Build completed in 7.009s

 DONE  Compiled successfully in 7013ms                                                                                                                                     21:25:22

Hash: 421d017116d2d95dd1e3
Version: webpack 3.12.0
Time: 7013ms
                                   Asset     Size  Chunks             Chunk Names
     pages/index.ef923f795c1cecc9a444.js  10.6 kB       0  [emitted]  pages/index
 layouts/default.87a49937c330bdd31953.js  2.69 kB       1  [emitted]  layouts/default
pages/our-values.f60c731d5c3081769fd9.js  3.03 kB       2  [emitted]  pages/our-values
   pages/join-us.835077c4e6b55ed1bba4.js   1.3 kB       3  [emitted]  pages/join-us
       pages/how.75f8cb5bc24e38bca3b3.js  2.59 kB       4  [emitted]  pages/how
             app.6dbffe6ac4383bd30a92.js   202 kB       5  [emitted]  app
          vendor.134043c361c9ad199c6d.js  6.31 kB       6  [emitted]  vendor
        manifest.421d017116d2d95dd1e3.js  1.59 kB       7  [emitted]  manifest
 + 3 hidden assets
Hash: 9fd206f4b4e571e9571f
Version: webpack 3.12.0
Time: 2239ms
             Asset    Size  Chunks             Chunk Names
server-bundle.json  306 kB          [emitted]  
  nuxt: Call generate:distRemoved hooks (1) +0ms
  nuxt:generate Destination folder cleaned +10s
  nuxt: Call generate:distCopied hooks (1) +8ms
  nuxt:generate Static & build files copied +7ms
  nuxt:render Rendering url /our-values +0ms
  nuxt:render Rendering url /how +67ms
  nuxt:render Rendering url /join-us +1ms
  nuxt:render Rendering url / +0ms
  nuxt: Call generate:page hooks (1) +913ms
  nuxt: Call generate:page hooks (1) +205ms
  nuxt: Call generate:page hooks (1) +329ms
  nuxt: Call generate:page hooks (1) +361ms
  nuxt:generate Generate file: /our-values/index.html +2s
  nuxt:generate Generate file: /how/index.html +0ms
  nuxt:generate Generate file: /join-us/index.html +0ms
  nuxt:generate Generate file: /index.html +0ms
  nuxt:render Rendering url / +2s
  nuxt: Call generate:done hooks (1) +4ms
  nuxt:generate HTML Files generated in 11.8s +5ms
  nuxt:generate Generate done +0ms
[21:25:27] Using gulpfile ~/scm/
[21:25:27] Starting 'deploy'...
Configured with Cloudfront distribution
[21:25:27] [cache]
[21:25:27] [cache]  android-chrome-192x192.png
[21:25:27] [cache]  android-chrome-512x512.png
[21:25:27] [cache]  apple-touch-icon.png
[21:25:27] [cache]  browserconfig.xml
[21:25:27] [cache]  favicon-16x16.png
[21:25:27] [cache]  favicon-32x32.png
[21:25:27] [cache]  favicon.ico
[21:25:27] [cache]  favicon.svg
[21:25:27] [cache]  logo-branches.svg
[21:25:27] [cache]  logo-small.svg
[21:25:27] [cache]  logo.svg
[21:25:27] [cache]  mstile-150x150.png
[21:25:27] [cache]  og-image.jpg
[21:25:27] [cache]  safari-pinned-tab.svg
[21:25:27] [cache]  site.webmanifest
[21:25:28] [create] _nuxt/manifest.421d017116d2d95dd1e3.js
[21:25:29] [update] 200.html
[21:25:30] [create] videos/flag.jpg
[21:25:30] [create] _nuxt/vendor.134043c361c9ad199c6d.js
[21:25:34] [create] videos/flag.mp4
[21:25:34] [cache]  _nuxt/pages/how.75f8cb5bc24e38bca3b3.js
[21:25:34] [cache]  _nuxt/pages/join-us.835077c4e6b55ed1bba4.js
[21:25:34] [cache]  _nuxt/pages/our-values.f60c731d5c3081769fd9.js
[21:25:36] [update] our-values/index.html
[21:25:36] [create] _nuxt/layouts/default.87a49937c330bdd31953.js
[21:25:36] [create] _nuxt/app.6dbffe6ac4383bd30a92.js
[21:25:37] [create] _nuxt/pages/index.ef923f795c1cecc9a444.js
[21:25:38] [update] join-us/index.html
[21:25:38] [update] how/index.html
[21:25:43] [create] videos/flag.webm
[21:25:43] [update] index.html
[21:25:43] Cloudfront invalidation created: I16NXXXXX4JDOA
[21:26:09] Finished 'deploy' after 42 s first runs nuxt generate, then runs gulp deploy with our environment variables set.

Note that the Cloudfront invalidation created: XXXX is the only output from the cloudfront invalidation npm package. If you don't see that, it's not working.

Caught a mistake or want to contribute to the documentation? Edit this page on GitHub!