Hosting a Hugo Blog on AWS with S3 and CloudFront
Building a serverless static site with HTTPS delivery
A walkthrough of how I set up this blog using Hugo and deployed it to AWS using S3 and CloudFront.
Project Overview
Goal: Host a personal technical blog on AWS using only serverless/managed services, keeping costs near zero while achieving fast global delivery over HTTPS.
Services Used:
- Amazon S3 – Static file storage and origin for the website
- Amazon CloudFront – CDN for HTTPS termination and global edge caching
- Hugo – Static site generator with the LoveIt theme
- AWS CLI – Deployment automation
Architecture
| |
Implementation
Setting Up Hugo
I chose Hugo as my static site generator because it’s fast, has no runtime dependencies, and the LoveIt theme provides a professional look out of the box with features like dark mode, table of contents, and code highlighting.
The basic workflow is:
- Write content in Markdown files under
content/posts/ - Run
hugo serverto preview locally - Run
hugo --minifyto build the production site intopublic/
S3 Static Website Hosting
I created an S3 bucket with static website hosting enabled:
| |
Then applied a public read bucket policy so CloudFront (and anyone) can access the files:
| |
CloudFront Distribution
I set up a CloudFront distribution to provide:
- HTTPS – S3 website endpoints only support HTTP, so CloudFront adds SSL/TLS
- Edge caching – Content cached at AWS edge locations worldwide for faster delivery
- Cache invalidation – After each deployment, I invalidate the cache to serve fresh content
The key configuration decision was using the S3 website endpoint (leestechblog.s3-website-eu-west-1.amazonaws.com) as the origin rather than the S3 REST API endpoint. This is important because the website endpoint automatically resolves directory URLs (like /about/) to index.html, which Hugo’s URL structure requires.
Deployment Script
I created a simple PowerShell script that handles the full deployment:
| |
What I Learned
- S3 website endpoints vs REST endpoints: The website endpoint handles directory index resolution, trailing slashes, and custom error pages. The REST endpoint doesn’t. When using CloudFront, this choice matters for how URLs resolve.
- CloudFront cache invalidation: The first 1,000 invalidation paths per month are free. Using
/*counts as one path and invalidates everything. - Hugo’s
baseURL: Setting this to/generates relative URLs that work regardless of the domain, which is useful when the final CloudFront URL isn’t known during development.
Cost Breakdown
| Service | Estimated Monthly Cost |
|---|---|
| S3 Storage | ~$0.02 (small static files) |
| S3 Requests | ~$0.01 |
| CloudFront | Free tier covers 1TB/month |
| Total | < $0.05/month |
Next Steps
- Add more project write-ups documenting AWS certification work
- Set up a CI/CD pipeline with GitHub Actions for automated deployments
- Consider adding a custom domain with Route 53 and ACM