Home β€Ί Blog β€Ί How to Deploy a Jekyll Site to AWS (S3 + CloudFront Guide)
Tutorial

How to Deploy a Jekyll Site to AWS (S3 + CloudFront Guide)

Host your Jekyll site on AWS S3 with CloudFront CDN β€” a complete guide covering S3 bucket setup, CloudFront distribution, custom domains with Route 53, SSL, and CI/CD with GitHub Actions.

How to Deploy a Jekyll Site to AWS (S3 + CloudFront Guide)

Deploying a Jekyll site to AWS gives you enterprise-grade infrastructure with granular control over every aspect of your hosting. The standard setup combines S3 (storage) with CloudFront (CDN) β€” effectively the same architecture that powers large commercial sites, available for a Jekyll blog at a cost that is often under $1 per month.

Why deploy Jekyll to AWS

  • Near-zero cost β€” S3 + CloudFront for a typical Jekyll blog costs $0.10–$2/month depending on traffic
  • Enterprise reliability β€” AWS S3 has 99.999999999% durability; CloudFront has 450+ global edge locations
  • Full control β€” custom cache policies, custom headers, Lambda@Edge for edge functions
  • No vendor lock-in β€” standard HTTP hosting, easy to migrate
  • Scales infinitely β€” handles 10 visitors or 10 million with the same configuration

The trade-off: setup is more involved than Netlify or Cloudflare Pages. It is the right choice when you need maximum control or are already using AWS for other infrastructure.

Architecture overview

Browser β†’ CloudFront (CDN, SSL, caching)
               ↓
          S3 Bucket (static file storage)

CloudFront serves files from the edge closest to each visitor. On cache miss, it fetches from S3. S3 stores your _site/ output.

AWS services used

  • S3 β€” stores your static files
  • CloudFront β€” CDN and HTTPS termination
  • ACM (Certificate Manager) β€” free SSL/TLS certificates
  • Route 53 (optional) β€” DNS management
  • IAM β€” access credentials for automated deploys

Step 1: Build your Jekyll site

JEKYLL_ENV=production bundle exec jekyll build

Verify _site/ contains your built output before proceeding.

Step 2: Create an S3 bucket

  1. Go to the AWS S3 console
  2. Click Create bucket
  3. Set Bucket name to your domain (e.g. jekyllhub.com) β€” use lowercase only
  4. Set AWS Region to the region closest to you (e.g. us-east-1)
  5. Under Block Public Access, uncheck Block all public access (CloudFront needs to read the files)
  6. Acknowledge the warning and click Create bucket

Enable static website hosting

  1. Click your bucket β†’ Properties tab
  2. Scroll to Static website hosting β†’ Edit
  3. Select Enable
  4. Set Index document: index.html
  5. Set Error document: 404.html
  6. Save

Note the Bucket website endpoint β€” you will need it for CloudFront.

Add a bucket policy for public read access

In your bucket β†’ Permissions tab β†’ Bucket policy β†’ Edit, paste:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::jekyllhub.com/*"
    }
  ]
}

Replace jekyllhub.com with your bucket name.

Step 3: Upload your site to S3

Using the AWS CLI (recommended β€” much faster than the console for many files):

# Install AWS CLI if not already installed
pip install awscli

# Configure with your credentials
aws configure

# Sync your _site/ folder to S3
aws s3 sync _site/ s3://jekyllhub.com   --delete   --cache-control "public, max-age=86400"   --exclude "*.html"   --exclude "*.xml"   --exclude "*.json"

# Upload HTML files with no-cache (so updates are seen immediately)
aws s3 sync _site/ s3://jekyllhub.com   --delete   --cache-control "no-cache, no-store, must-revalidate"   --include "*.html"   --include "*.xml"   --include "*.json"

This two-pass approach caches assets (CSS, JS, images) aggressively while ensuring HTML is always fresh.

Step 4: Request an SSL certificate in ACM

  1. Go to the ACM console β€” must be in us-east-1 (CloudFront requires this)
  2. Click Request a certificate β†’ Request a public certificate
  3. Add your domain names: jekyllhub.com and www.jekyllhub.com
  4. Choose DNS validation
  5. Click Request
  6. Add the CNAME records shown to your DNS provider
  7. Wait for status to show Issued (usually 5–30 minutes)

Step 5: Create a CloudFront distribution

  1. Go to the CloudFront console
  2. Click Create distribution
  3. Under Origin:
    • Origin domain: Paste your S3 website endpoint (not the bucket ARN β€” use the website endpoint that looks like jekyllhub.com.s3-website-us-east-1.amazonaws.com)
    • Protocol: HTTP only (S3 website endpoints do not support HTTPS origin β€” CloudFront handles SSL at the edge)
  4. Under Default cache behavior:
    • Viewer protocol policy: Redirect HTTP to HTTPS
    • Allowed HTTP methods: GET, HEAD
    • Cache policy: CachingOptimized
  5. Under Settings:
    • Alternate domain names (CNAMEs): Add jekyllhub.com and www.jekyllhub.com
    • Custom SSL certificate: Select the ACM certificate you just created
    • Default root object: index.html
  6. Click Create distribution

The distribution takes 5–15 minutes to deploy globally. You will see a CloudFront domain like d1234abcd.cloudfront.net.

Step 6: Configure DNS

If using Route 53:

  1. Create a hosted zone for your domain
  2. Create an A record as an alias pointing to your CloudFront distribution
  3. Update your domain registrar to use Route 53 nameservers

If using another DNS provider:

  • Create a CNAME record: www β†’ d1234abcd.cloudfront.net
  • For the root domain, use your registrar’s ALIAS or ANAME feature pointing to the CloudFront domain

Step 7: Handle clean URLs and 404s

Jekyll generates about/index.html for a page at /about/. CloudFront serves index.html at the root but not in subdirectories by default β€” navigating directly to /about/ returns a 403.

Fix this with a CloudFront Function:

  1. Go to CloudFront β†’ Functions β†’ Create function
  2. Name it jekyll-url-rewrite
  3. Paste this code:
function handler(event) {
  var request = event.request;
  var uri = request.uri;

  // Add index.html to directory requests
  if (uri.endsWith("/")) {
    request.uri += "index.html";
  } else if (!uri.includes(".")) {
    request.uri += "/index.html";
  }

  return request;
}
  1. Click Save changes β†’ Publish
  2. In your CloudFront distribution β†’ Behaviors β†’ Edit β†’ Function associations β†’ Viewer request β†’ select your function

This rewrites /about/ to /about/index.html at the edge before CloudFront looks up the file.

Step 8: Automate deploys with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy Jekyll to AWS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.2"
          bundler-cache: true

      - name: Build Jekyll site
        run: JEKYLL_ENV=production bundle exec jekyll build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: us-east-1

      - name: Sync to S3 (assets β€” long cache)
        run: |
          aws s3 sync _site/ s3://$             --delete             --cache-control "public, max-age=31536000, immutable"             --exclude "*.html"             --exclude "*.xml"             --exclude "*.json"             --exclude "*.txt"

      - name: Sync to S3 (HTML β€” no cache)
        run: |
          aws s3 sync _site/ s3://$             --cache-control "no-cache, no-store, must-revalidate"             --include "*.html"             --include "*.xml"             --include "*.json"             --include "*.txt"

      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation             --distribution-id $             --paths "/*"

Add these secrets to your GitHub repository:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • S3_BUCKET (your bucket name)
  • CLOUDFRONT_DISTRIBUTION_ID

Create an IAM user for GitHub Actions

Never use your root AWS credentials in GitHub Actions. Create a dedicated IAM user:

  1. IAM β†’ Users β†’ Create user β†’ name it jekyll-deploy
  2. Attach this policy directly:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::jekyllhub.com",
        "arn:aws:s3:::jekyllhub.com/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
    }
  ]
}
  1. Create access keys for this user and store them as GitHub secrets.

Cost estimate

For a typical Jekyll blog with 10,000 monthly visitors:

Service Monthly cost
S3 storage (100MB) ~$0.002
S3 requests ~$0.05
CloudFront data transfer (1GB) ~$0.085
CloudFront requests (100k) ~$0.008
ACM certificate Free
Route 53 hosted zone $0.50
Total ~$0.65/month

CloudFront has a free tier: 1TB data transfer and 10 million requests per month free for the first 12 months.

Troubleshooting

403 Forbidden on CloudFront Check the bucket policy allows s3:GetObject for all principals, and you used the S3 website endpoint (not the REST endpoint) as the CloudFront origin.

Subdirectory pages return 403 or 404 Ensure you have the CloudFront Function rewriting URLs to index.html β€” see Step 7.

Old content still showing after deploy The CloudFront cache invalidation in the GitHub Actions workflow clears this. If running manually, create an invalidation for /* in the CloudFront console.

SSL certificate not available in CloudFront ACM certificates must be in the us-east-1 region to be usable with CloudFront. If you created it in another region, request a new one in us-east-1.

AWS S3 + CloudFront is the most scalable and cost-effective way to host a Jekyll site when you need full control over your infrastructure. The setup is more complex than Netlify or Cloudflare Pages, but once the GitHub Actions workflow is in place, deployments are fully automated.

Share LinkedIn