{ Simple Frontend }

How to deploy a versioned app to s3

Learn how to safely deploy a versioned static app to S3 with one folder per release allowing instant rollbacks.

Jeremy Colin Jeremy Colin
Oct 2, 2025 - 4 min read

S3 + CloudFront is great for deploying static apps

S3 is extremely cheap to store a large amount of static assets. CloudFront puts your app at the edge with all the good defaults packed-in such as CDN caching and top-notch compression.

Just make sure you mark your bucket private with access control for CloudFront only and you’re good to go.

These are exactly the use cases we had at Storyblok where after years of uploading assets, the aws s3 sync command took more than 5 minutes to upload a few MB of static assets. Rolling back to a previous version was also slow as we needed to rebuild the app and re-upload the assets.

slow aws s3 upload taking 6min 57s

What we understood is that the aws s3 sync command walks all keys under the target prefix to decide what to upload. This gets slow over time when you upload a lot of files to the same destination prefix.

Fast uploads and rollbacks with release folders

1. index.html as our current release pointer

We had kep things things simple which was one advantage: our application is completely Client Side Renderered (CSR) and the single entry point is the root bucket index.html file we upload.

It contains the reference to further static assets the browser needs to download to render our app. So when we decided to version our releases, the index.html was chosen as our release “pointer”.

2. one folder per release in our S3 bucket

Since we wanted to keep our S3 upload fast, we decided to isolate each release into a seperate folder to speed up the upload of our assets.

As we wanted to keep things simple and have the root bucket index.html stay our bucket entrypoint, we had to adapt the assets path based on our releases. Before we were serving assets directly from the bucket /assets path like so:

<script type="module" src="/assets/index-[hash].js" />

and now that had to be updated to:

<script type="module" src="/assets/[release-id]/index-[hash].js" />

Fortunately with a bundler like Vite that’s straightforward to do using the base config parameter: { base: releaseId }

3. S3 upload during releases

We configured our release id in an env.tag variable in Github action and our updated upload commands looked like this:

Terminal window
# upload all build assets from dist/ to s3 release folder
aws s3 sync dist/ s3://${{ env.S3_BUCKET }}/${{ env.tag }}/
# upload index.html to bucket root
aws s3 cp dist/index.html s3://${{ env.S3_BUCKET }}/index.html

And now you can see the upload is only taking 18 seconds:

fast aws s3 upload taking 18s

4. S3 bucket folder structure

This is how our bucket folders and files looks like now:

root/
│── index.html
└── 1.0.0/
│ │-- index.html
│ └───assets/
│ │ index-[hash].js
│ │ index-[hash].css
│ │ ...
└───-1.1.0/
│ │-- index.html
│ └─── assets/
│ │ index-[hash].js
│ │ index-[hash].css
│ │ ...

5. Fast rollbacks

The final piece of the puzzle was to enable fast rollbacks. With this setup, it was a breeze to do, we simply copy the index.html from the release we want to rollback to the root of the bucket:

Terminal window
# Rollback to previous version
aws s3 cp s3://${{ env.S3_BUCKET }}/${{ env.tag }}/index.html s3://${{ env.S3_BUCKET }}/index.html

And the whole operation was now taking less than a minute (including the CloudFront invalidation):

fast rollback by coping release index.html to bucket root

6. Do not forget cache invalidations

Depending on your caching strategy, you might need to invalidate your root index.htnl for your distribution but that might not even be needed if you have a short time to live (TTL) or no cache for your index.html.

Terminal window
aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION_ID" --paths "/"

Note that here the / path invalidates only the distribution default root objet, in our case the index.html.

I hope you found this guide useful and if you have any questions or suggestions, feel free to reach out.