AWS Builder Center
Host a Fast, Free Portfolio Website with S3 and CloudFront

Host a Fast, Free Portfolio Website with S3 and CloudFront

A step-by-step guide to hosting a static portfolio on Amazon S3 and serving it globally through CloudFront — using AWS's current security setup, not the outdated public-bucket tutorial still floating around online.

Tech Enthusiast

What you're building

A personal portfolio site — HTML, CSS, JS, maybe a resume PDF — served
fast to visitors anywhere in the world, over HTTPS, on your own domain
if you want one. Two AWS services do the work:
  • Amazon S3 stores your actual files. Think of it as a folder in
    the cloud, not a web server.
  • Amazon CloudFront is a CDN (content delivery network) — it copies
    your files to servers ("edge locations") around the world, so a
    visitor in Singapore isn't waiting on a request to travel to
    wherever your S3 bucket actually lives.
S3 alone can serve files, but every visitor everywhere would be
fetching directly from one region — slow for anyone far from it, and
harder to get free HTTPS on a custom domain. CloudFront in front of S3
fixes both.

What you'll need

  • An AWS account (the free tier covers this comfortably)
  • Your finished site files (index.html, CSS, JS, images — built by
    hand, or output from something like a static site generator)
  • Optional: a domain name, if you want yourname.dev instead of a
    long cloudfront.net address

Step 1: Create a private S3 bucket

In the S3 console, create a bucket. Bucket names are globally unique,
so my-portfolio is almost certainly taken — try something like
yourname-portfolio-2026.
Leave "Block all public access" turned ON. This is the part that
differs from older tutorials — we're deliberately keeping the bucket
private. CloudFront will be the only thing allowed to read from it.

Step 2: Upload your site files

Upload your files into the bucket, keeping the same folder structure
your site uses locally (index.html at the root, an /assets/ folder
if you have one, and so on).

Step 3 (optional): Request an HTTPS certificate

Only needed if you want a custom domain. In AWS Certificate Manager
(ACM)
, request a public certificate for your domain — with one
non-obvious rule: the certificate must be requested in the
us-east-1 (N. Virginia) region
, regardless of where you or your
users actually are. CloudFront only accepts certificates from that
region. Validate it via DNS (ACM will give you a CNAME record to add).

Step 4: Create the CloudFront distribution

In the CloudFront console, create a distribution:
  • Origin domain: select your S3 bucket. CloudFront will offer two
    connection types — pick the REST API endpoint (the default option
    when you select the bucket directly), not the separate "website
    endpoint."
  • Origin access: choose Origin access control (OAC), then
    create a new OAC. This is what lets CloudFront read your private
    bucket while everyone else is locked out.
  • Viewer protocol policy: redirect HTTP to HTTPS.
  • Alternate domain name (CNAME): your custom domain, if using one
    — plus the ACM certificate from Step 3.
After creating the distribution, CloudFront shows you a bucket policy
statement. Copy it and paste it into your S3 bucket's permissions →
bucket policy.
This is the step that actually grants CloudFront
access — skipping it is the most common reason a fresh setup returns
"Access Denied."

Step 5: Set the default root object

Still in the distribution settings, set default root object to
index.html. Without this, visiting your bare domain (no filename)
returns an error instead of your homepage.

Step 6: Fix the "pretty URL" gap

Here's a gap most tutorials skip. S3's old "static website hosting"
feature used to auto-handle folder-style URLs (/projects/ → serves
/projects/index.html). The private-bucket-plus-OAC setup doesn't do
that automatically — CloudFront will return an access error for any
URL that doesn't point to an exact file.
Fix it with a small CloudFront Function (Functions → Create
function, attach it to your distribution's default cache behavior as
a viewer request function):
1
2
3
4
5
6
7
8
9
10
11
12
function handler(event) {
var request = event.request;
var uri = request.uri;

if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}

return request;
}
This rewrites /projects/ and /projects into
/projects/index.html before the request reaches S3.
If your portfolio is a single-page app (React, Vue, etc.) rather than
plain HTML pages, you'll also want to set a custom error response
in the distribution: for HTTP 403 and 404, respond with /index.html
and an HTTP status of 200. That lets your app's own client-side router
handle the URL instead of AWS returning an error page.

Step 7: Point your domain at CloudFront

If you're using a custom domain through Route 53, create an A
record
(and AAAA, for IPv6) of type Alias, pointing at your
CloudFront distribution. Alias records are Route 53's way of pointing
a domain directly at an AWS resource without needing a fixed IP
address.

Step 8: Updating your site later

When you change your site, upload the new files to S3 — but
CloudFront caches content at the edge, so visitors might keep seeing
the old version for a while. To force it to grab the new files
immediately, create a cache invalidation for the changed paths (or
/* for everything) from the CloudFront console or CLI:
1
2
3
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"

What this actually costs

For a personal portfolio, realistically close to nothing. < cite index="20-1">CloudFront's Always Free tier includes 1 TB of data transfer out and 10 million HTTP/HTTPS requests per month,</cite> and < cite index="25-2">that free allowance covers CloudFront Functions too, at 2 million invocations a month</cite> — the function from Step 6 included. On top of that, < cite index="23-3">data transferred from an S3 origin to CloudFront doesn't cost anything at all</cite> — the only realistic charge is a few cents a month for S3 storage of your actual files. A portfolio site would need to go seriously viral to ever see a bill.

Common mistakes to avoid

  • Using the S3 "static website hosting" checkbox with a public
    bucket.
    It's the tutorial you'll find most often, and it's the
    outdated approach — < cite index="17-1">a publicly accessible S3 bucket behind CloudFront can be hit directly by attackers, bypassing CloudFront's caching and protections entirely, which has led to real, costly billing spikes.</cite> OAC exists specifically to prevent this.
  • Forgetting to paste the bucket policy CloudFront generates.
    Creating the OAC isn't enough by itself — S3 also needs to be told
    to trust it.
  • Forgetting cache invalidation after updates. Your new files are
    in S3, but edge locations may keep serving the old cached version
    until you invalidate or the cache naturally expires.
  • Requesting the ACM certificate in the wrong region. If
    CloudFront can't find your certificate in the dropdown, it's almost
    always this.

Where to go from here

Once this is working, natural next steps: automate deployment with a
GitHub Actions workflow that uploads to S3 and invalidates the cache
on every push, add AWS WAF if you want extra protection, or move from
a hand-coded site to a static site generator for easier content
updates. All of them build on exactly the S3-plus-CloudFront
foundation you just set up.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article