Table of Contents
When I built this blog, getting it running on my own computer was the easy part. Run one command, open a browser, done. The part that felt like a mountain was the next question: how do you take a folder of files on your laptop and turn it into a real website that anyone in the world can visit?
This post is the walkthrough I wish I’d had. No prior cloud experience needed. If you already know your way around DNS and S3, you can skim the steps and grab the Namecheap-specific gotchas. If you’re newer, I’ve linked out to deeper explainers at each tricky part so you can actually understand what you’re doing, not just copy commands. That’s the whole spirit of this blog: do it yourself, and learn how it works while you’re at it.
What we’re building
The site is a static site — just HTML, CSS, images, and a tiny bit of JavaScript. There’s no server running code, no database. That makes it cheap and simple to host. Here’s the cast of characters:
- S3 — cloud storage that holds the site’s files
- CloudFront — a content delivery network (CDN) that copies the site to servers around the world so it loads fast, and handles the HTTPS padlock
- Certificate Manager — gives you a free SSL certificate (that’s the padlock)
- Route 53 or your registrar — connects your domain name to all of the above
Think of it like this: S3 is the warehouse where your stuff lives, CloudFront is the fleet of local delivery trucks parked all over the world, the certificate is the tamper-proof seal on the box, and DNS is the address label that tells visitors where to go.
Step 1: Set up your AWS account safely
Once you’ve created your AWS account, do two things before anything else:
- Turn on MFA (multi-factor authentication). Your account is tied to a credit card, so a password alone isn’t enough. Use an authenticator app on your phone.
- Set a billing alarm. Go to the Billing console, create a monthly budget, and set it to something like $20. Now AWS will email you if charges ever approach that. For a small blog, you’ll actually pay around $2–3/month, so this is just a safety net.
Why bother? An unsecured cloud account with your card on file is exactly the kind of thing you don’t want to leave lying around. Five minutes here saves a lot of worry later.
Step 2: Make an IAM user (don’t use root)
When you first sign up, you’re logged in as the root user — an account that can do anything, including changing billing and closing the account. You don’t want to hand that level of power to a command line tool. So before installing anything, create a limited IAM user for day-to-day work.
In the AWS console, go to IAM → Users → Create user. Name it something like imadestuff-deploy, skip the console-login option (this user only needs CLI access), and attach a permissions policy. I started with AdministratorAccess to avoid permission roadblocks while setting up, with a note to tighten it down to just S3, CloudFront, and ACM once the site was live.
Then create an access key for that user (in its Security credentials tab) and choose Command Line Interface (CLI) as the use case.
Step 3: Install the AWS CLI
The AWS CLI (command line interface) lets you control AWS from your terminal. On Windows you can install it with:
winget install Amazon.AWSCLI
Then verify it worked:
aws --version
Now connect the CLI to your account using the access key from the previous step:
aws configure
Paste in your key ID and secret, set the region to us-east-1, and choose json for the output format. Confirm it’s working with:
aws sts get-caller-identity
That should print your account ID and the IAM user’s name — proof the CLI is authenticated as your deploy user.
Step 4: Create the S3 bucket
S3 is where the site’s files live. The bucket name has to match your domain exactly — so mine is named imadestuff.com.
- In the S3 console, create a bucket named after your domain
- Uncheck “Block all public access” — your blog is public, so visitors need to read the files
- Enable static website hosting in the bucket’s Properties, with
index.htmlas the index document and404.htmlas the error document - Add a bucket policy that allows public read access
That bucket policy looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::imadestuff.com/*"
}
]
}
After enabling website hosting, S3 gives you a website endpoint URL. Copy it — you need it for the next step, and it’s different from the regular bucket URL in a way that matters.
Step 5: Set up CloudFront
CloudFront is the CDN. It caches your site on servers worldwide and gives visitors a fast, secure connection.
When you create the distribution, the one trick that trips everyone up: for the origin, paste your S3 website endpoint (the one from Step 4, like imadestuff.com.s3-website-us-east-1.amazonaws.com). Don’t pick the bucket from the dropdown — that’s a different URL and it’ll break links to pages like /about/.
A few other settings:
- Viewer protocol policy: Redirect HTTP to HTTPS
- Default root object:
index.html - Add a custom error response so 404s serve your
/404.htmlpage
CloudFront takes a few minutes to deploy. While it does, grab the Distribution ID and distribution domain name (something like d1234abcdef8.cloudfront.net) — you’ll need both shortly.
Step 6: Get a free SSL certificate
This is the padlock in the address bar. AWS Certificate Manager gives them out free.
Request a public certificate for both imadestuff.com and *.imadestuff.com (the wildcard covers www and any future subdomains). Choose DNS validation, then add the CNAME record AWS gives you to your domain’s DNS. Once it validates (usually 5–30 minutes), attach the certificate to your CloudFront distribution and add your domain names as alternate domain names (CNAMEs).
A note on adding records at Namecheap
My domain is registered at Namecheap, and there’s one quirk worth knowing. When you add the validation CNAME in Advanced DNS → Host Records, the Host field should contain only the first chunk of the record name — the part before your domain. Namecheap automatically appends .imadestuff.com for you. So if AWS gives you a record named _abc123.imadestuff.com, you enter just _abc123 in the Host field. Paste the whole thing and it ends up doubled (_abc123.imadestuff.com.imadestuff.com) and won’t validate.
The Value field, on the other hand, takes the full target string exactly as AWS provides it, trailing dot and all.
Step 7: Point your domain at CloudFront
This is the step that makes typing imadestuff.com actually land on your site. It’s also the step nobody really explains, so let’s go deeper — because once you understand what’s happening here, the whole internet makes more sense.
What actually happens when you type a URL
Here’s the thing that took me embarrassingly long to internalize: your domain name and your website are two completely separate things. You buy the name from one company (a registrar, like Namecheap). You store the website somewhere else (in our case, AWS). Nothing automatically connects them. That connection is a job you have to set up, and it’s called DNS.
So what happens in the half-second after you hit Enter on imadestuff.com?
- Your browser has a name (
imadestuff.com) but computers don’t talk in names — they talk in numbers called IP addresses (like192.64.119.233). So the first thing it does is ask, “what’s the address for this name?” - That question goes to the DNS system — think of it as the phone book of the internet. It’s a giant, distributed lookup service whose entire job is translating human-friendly names into machine-friendly addresses.
- DNS answers with an address (or, as we’ll see, another name to look up). Your browser then connects to that address and asks for the page.
- The server at that address — CloudFront, for us — sends back your HTML, CSS, and images, and the browser paints the page.
The magic word is “resolve.” When people say a domain “resolves,” they mean that lookup in step 2 succeeded. When you set up a fresh domain, it doesn’t resolve to your site yet because you haven’t told the phone book where to send people. That’s what we’re about to do.
Where DNS records live
Every domain has a set of DNS records — the actual entries in the phone book. They live wherever your domain’s nameservers point. When you register a domain at Namecheap, its nameservers default to Namecheap, which means you manage your records in Namecheap’s dashboard (Advanced DNS → Host Records). That’s why we’re doing this at the registrar and not inside AWS.
(You can hand DNS duties over to AWS’s own service, Route 53, by changing your nameservers. It integrates a little more smoothly with CloudFront, but it’s an extra moving part and costs $0.50/month. For one domain, managing records right at Namecheap is perfectly fine.)
The records we’re adding, and why they’re different
We need two versions of the site to work: imadestuff.com (the bare “root” domain) and www.imadestuff.com. Both should land on the same CloudFront distribution. But they use different record types, and this is the part that confuses everyone.
A quick tour of the record types you’ll meet:
| Record | What it does |
|---|---|
| A | Points a name directly at a numeric IPv4 address (93.184.x.x) |
| AAAA | Same idea, but for newer IPv6 addresses |
| CNAME | Points a name at another name instead of a number (“go ask about d123.cloudfront.net instead”) |
| ALIAS | A special CNAME-like record that’s allowed where a plain CNAME isn’t |
We use a CNAME because CloudFront doesn’t give us a fixed IP address to point at — it gives us a name (d1ue5ah0u2bmm4.cloudfront.net), and that name resolves to different servers depending on where the visitor is in the world. A CNAME says “whatever that name points to, point here too,” so it keeps working even as CloudFront’s underlying addresses change.
Here’s the catch that has tripped up developers for decades: a plain CNAME is not allowed on the root of a domain (the bare imadestuff.com, written as host @). It’s a rule baked into how DNS was originally designed — the root domain has to carry certain other records that a CNAME would clobber. So you simply cannot put a CNAME on @.
ALIAS is the workaround. It’s a registrar-level trick that behaves like a CNAME but is legal on the root domain. Namecheap supports it (not every registrar does — this is one reason Namecheap and Cloudflare are pleasant to use). So:
- ALIAS on host
@→ handles the bareimadestuff.com - CNAME on host
www→ handleswww.imadestuff.com(a subdomain, where a normal CNAME is perfectly legal)
Both point at the exact same CloudFront address. You’re just teaching both spellings of your domain to go to the same place.
The actual records
In Namecheap’s Advanced DNS → Host Records, first delete the default parking entries (the CNAME for www pointing at parkingpage.namecheap.com, and any URL-redirect record on @) or they’ll fight with the new ones. Leave the certificate-validation CNAME from Step 6 alone — that one keeps your HTTPS renewing.
Then add these two:
| Type | Host | Value |
|---|---|---|
| ALIAS Record | @ | d1234abcdef8.cloudfront.net |
| CNAME Record | www | d1234abcdef8.cloudfront.net |
Why it’s not instant
After you save, the site probably won’t work immediately, and that’s normal. DNS answers get cached — your computer, your router, and servers all over the internet remember the old answer for a while so they don’t have to ask every single time. That caching duration is the TTL (time to live). Until the old cached answers expire, some places will still see “no site here.” This spreading-out is called propagation, and it can take anywhere from a few minutes to a couple of hours (occasionally up to a day). You can watch it happen with a tool like dnschecker.org, which shows whether your record is visible from different countries yet.
If you want to understand caching and TTLs properly, this Cloudflare explainer on DNS caching covers it well.
Step 8: Deploy
With everything wired up, deploying is the easy part. I set my CloudFront distribution ID as an environment variable and run the deploy script:
export CF_DISTRIBUTION_ID="E1A2B3C4D5E6F7"
./deploy.sh
The script does three things: builds the site with hugo --minify, uploads the files to S3, and tells CloudFront to refresh its cache so visitors see the latest version. After a minute or two, the new content is live worldwide.
What it costs
For a low-traffic blog (under 10,000 views a month), the whole thing runs about $2–3/month:
| Service | Monthly cost |
|---|---|
| S3 storage + requests | ~$0.50 |
| CloudFront | ~$1–2 |
| Route 53 (if used) | $0.50 |
| SSL certificate | Free |
Costs scale gently with traffic — even at 100,000 views a month you’d likely stay under $10. The billing budget from Step 1 means you’ll never get a nasty surprise.
Was it worth it?
Honestly, yes. The first time I typed imadestuff.com into my phone’s browser and watched my own site load — with the little padlock, the neon glow, robots and all — it felt incredible. This thing I made on my laptop was suddenly real, on the actual internet, with a domain name I picked.
The setup looks intimidating as a wall of steps, but each one is small, and most of it you only ever do once. After that, publishing a new post is a single command:
./deploy.sh
Twelve seconds later it’s live worldwide.
If you’re sitting on a finished site that only you can see, this is the bridge to the rest of the world. Give it a weekend afternoon. The setup is more approachable than it looks, the hosting costs less than a coffee, and the feeling of seeing your own URL actually work is worth every step.
Now go make stuff — and put it on the internet for people to find.
Comments