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.
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
- Go to the AWS S3 console
- Click Create bucket
- Set Bucket name to your domain (e.g.
jekyllhub.com) β use lowercase only - Set AWS Region to the region closest to you (e.g.
us-east-1) - Under Block Public Access, uncheck Block all public access (CloudFront needs to read the files)
- Acknowledge the warning and click Create bucket
Enable static website hosting
- Click your bucket β Properties tab
- Scroll to Static website hosting β Edit
- Select Enable
- Set Index document:
index.html - Set Error document:
404.html - 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
- Go to the ACM console β must be in us-east-1 (CloudFront requires this)
- Click Request a certificate β Request a public certificate
- Add your domain names:
jekyllhub.comandwww.jekyllhub.com - Choose DNS validation
- Click Request
- Add the CNAME records shown to your DNS provider
- Wait for status to show Issued (usually 5β30 minutes)
Step 5: Create a CloudFront distribution
- Go to the CloudFront console
- Click Create distribution
- 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)
- Origin domain: Paste your S3 website endpoint (not the bucket ARN β use the website endpoint that looks like
- Under Default cache behavior:
- Viewer protocol policy: Redirect HTTP to HTTPS
- Allowed HTTP methods: GET, HEAD
- Cache policy: CachingOptimized
- Under Settings:
- Alternate domain names (CNAMEs): Add
jekyllhub.comandwww.jekyllhub.com - Custom SSL certificate: Select the ACM certificate you just created
- Default root object:
index.html
- Alternate domain names (CNAMEs): Add
- 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:
- Create a hosted zone for your domain
- Create an A record as an alias pointing to your CloudFront distribution
- 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:
- Go to CloudFront β Functions β Create function
- Name it
jekyll-url-rewrite - 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;
}
- Click Save changes β Publish
- 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_IDAWS_SECRET_ACCESS_KEYS3_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:
- IAM β Users β Create user β name it
jekyll-deploy - 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"
}
]
}
- 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.