How I host a static MkDocs site on AWS¶
And what I learn on the way about architecture, security, and cost control¶
I tried Amazon Lightsail and WordPress first — the cost was too high. I wanted something simple: a light, static site that I could build myself. The idea came from the FastAPI documentation, which is built with MkDocs and hosted fully on AWS.
But this is not only about publishing a site. It is also a way to learn. To understand the cloud from the inside — through practice, decisions, mistakes, and fixes.
What I chose, and why¶
| Component | Description |
|---|---|
| MkDocs | A static site generator — fast, light, great for a blog |
| AWS S3 | Stores the generated HTML, CSS, and JS files |
| Amazon CloudFront | Delivers the site worldwide, handles HTTPS and caching |
| AWS Route 53 | DNS for my domain, andrzejoblong.pl |
| ACM (SSL) | SSL certificate for HTTPS security |
| AWS WAF | Protection against abuse and bots |
| AWS Budgets | Cost alerts, so the bill never surprises me |
How I secured it¶
S3 bucket¶
- Public access to S3: BLOCKED!
- A bucket policy with the condition
AWS:SourceArn == CloudFront
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<BUCKET_ID>/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "<CLOUDFRONT_DISTRIBUTION_ARN>"
}
}
}
]
}
CloudFront¶
- OAC (Origin Access Control): only CloudFront can read the files
- Object compression
- Redirect from HTTP to HTTPS
- Only GET and HEAD methods are allowed for my site
- Cache key and origin request policies: a) CachingOptimized b) CORS-S3Origin
- TTL of 24 hours in the cache policy
- SSL certificate (ACM) plus the domain in Route 53
- Simpler navigation thanks to a CloudFront Function, which automatically adds "index.html" to URLs that have no file extension:
function handler(event) {
var request = event.request;
var uri = request.uri;
// Check whether the URI is missing a file name.
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
// Check whether the URI is missing a file extension.
else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
Thanks to this function, I can write links like /about, /docs, /blog/, and CloudFront finds the right files in S3 — with no errors.
- WAF with a
Rate-based rule
Estimated monthly cost¶
| Service | Cost (USD) | Notes |
|---|---|---|
| S3 (storage) | 0.01–0.10 | Static files |
| S3 (GET requests) | 0.01–0.50 | Depends on the number of visits |
| CloudFront | 1–3 | Transfer plus cache |
| Route 53 | 0.50 | DNS and domain |
| ACM SSL | 0.00 | Free |
| WAF | 5 + 1 USD/rule/month | |
| TOTAL | ~8 USD | For a light site |
What I learn from this project¶
- Designing CDN, cache, and access control together
- Managing costs consciously
- Separating infrastructure from content
- Building real cloud architecture awareness
My personal checklist¶
- S3: no public access
- CloudFront: active cache, SSL
- OAC: only CloudFront can reach S3
- Route 53: domain set up
- WAF: rate limit, bot protection
- Budgets: cost alert at 5 USD
Areas to improve¶
Deployment automation¶
mkdocs build
The generated files go into site/.
Upload the site to S3:
aws s3 sync site/ s3://<BUCKET_ID>/ --delete
Invalidate the CloudFront cache:
aws cloudfront create-invalidation \
--distribution-id <MY_ID> \
--paths "/index.html" "/"
- A script to automate this
- Deploy automation with GitHub Actions
- Terraform to manage the infrastructure
Summary¶
I am not building the cheapest setup. I am building it consciously — for myself. To learn, to test, to grow. This is a project where data and decisions meet responsibility.