S3: Store Files and Host a Static Website
In this article we use S3 to store files and host a static website. The difference from EC2 is that no server runs continuously: you just push files up and AWS handles the serving.
S3 (Simple Storage Service) is an object storage service. It's not a disk or an ordinary file system but a flat file store: each file is an object, placed inside a bucket. S3 is a good fit for storing images, videos, backup files, downloads, and for hosting static web content.
Goals
- Create an S3 bucket.
- Upload files to the bucket via both the UI and the CLI.
- Enable static website hosting and open the site on the Internet.
- Clean up the bucket when done.
Estimated cost
S3 bills by storage volume, number of requests, and amount of data transferred out.
- Credit-model account (created after 2025-07-15): the cost is deducted from the granted credit. A few HTML files cost thousandths of a cent, essentially zero.
- Old-model account (created before 2025-07-15): 5 GB of storage, 20,000 GET requests, and 2,000 PUT requests per month are free for the first 12 months.
Either way, the few files in this article are practically negligible cost-wise. We still clean up at the end, because a bucket left around for a long time easily becomes a forgotten place.
Concepts: bucket and object
- A bucket is the top-level container. The bucket name must be globally unique — not just within your account but across all of AWS. So names like
testormy-bucketare certain to collide; give it a distinctive part, for exampledevops-series-<your-name>-2025. - An object is a file you store in a bucket, with a key (like a path). An object
images/logo.pnghas the keyimages/logo.png— the/is just part of the name; S3 has no real folders and only displays them as folders for readability.
By default, every bucket and object is private: only your account can access them. To let outsiders view them (as when hosting a site), you have to deliberately open access. This private-by-default is intentional, because data leaks from accidentally public buckets are one of the most common security incidents in the cloud.
Step 1: Create a bucket
Set a bucket name variable up front to reuse (change the suffix to make it unique):
BUCKET=devops-series-yourname-2025
aws s3 mb s3://$BUCKET --region ap-southeast-1
s3 mb means "make bucket". If it reports the name already exists, switch to a more unique name.
Check the bucket exists:
aws s3 ls
Step 2: Upload files
Create a few static files on your machine to upload:
mkdir site && cd site
cat > index.html <<'EOF'
<!doctype html>
<html lang="vi">
<head><meta charset="utf-8"><title>Trang tinh tren S3</title></head>
<body>
<h1>Xin chao tu S3</h1>
<p>Trang nay duoc host truc tiep tu mot S3 bucket.</p>
<p><a href="about.html">Trang gioi thieu</a></p>
</body>
</html>
EOF
cat > about.html <<'EOF'
<!doctype html>
<html lang="vi">
<head><meta charset="utf-8"><title>Gioi thieu</title></head>
<body><h1>Gioi thieu</h1><p>Day la trang thu hai.</p></body>
</html>
EOF
Upload one file:
aws s3 cp index.html s3://$BUCKET/
Or sync the whole directory up to the bucket:
aws s3 sync . s3://$BUCKET/
s3 sync only uploads files that are new or changed, so it's handy when updating the site. List the objects in the bucket:
aws s3 ls s3://$BUCKET/
At this point the files are on S3 but still private — opening them by public URL will be denied. The next step is what opens them to the Internet.
Step 3: Enable static website hosting
S3 can serve static content directly like a web server. Enable this feature:
aws s3 website s3://$BUCKET/ \
--index-document index.html \
--error-document index.html
This command sets the default page (index.html) and the page shown on errors.
For outsiders to view it, we need two things: remove the public access block, then attach a bucket policy that allows reads.
Remove "Block Public Access" for this bucket:
aws s3api put-public-access-block \
--bucket $BUCKET \
--public-access-block-configuration \
"BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"
Create a policy file allowing everyone to read objects in the bucket:
cat > policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::$BUCKET/*"
}
]
}
EOF
aws s3api put-bucket-policy --bucket $BUCKET --policy file://policy.json
This policy only allows reads (s3:GetObject), not writes or deletes. Outsiders can view the website but can't modify your files.
The website address looks like:
http://<BUCKET>.s3-website-<region>.amazonaws.com
With the example above (region ap-southeast-1):
http://devops-series-yourname-2025.s3-website-ap-southeast-1.amazonaws.com
The endpoint format differs by region: some regions use a dash (
s3-website-<region>), others use a dot (s3-website.<region>). To be sure, grab the exact URL from the console: open the bucket, the Properties tab, scroll down to Static website hosting — AWS shows the exact URL there.
Open this address in a browser and you'll see the index.html page, with the link to about.html working too. Updating the site later just means editing the file and running aws s3 sync . s3://$BUCKET/ again.
In a real environment, few people leave a bucket directly public. The common approach is to put a CDN (CloudFront) in front, keep the bucket private, and let only CloudFront read it. This is faster (with a global cache) and supports HTTPS. The series stays at the S3 website level for simplicity; when you need production, look into CloudFront combined with Origin Access Control.
🧹 Cleanup
S3 bills by storage volume, so you need to delete both the objects and the bucket. A bucket with objects can't be deleted, you have to clear the objects first.
The fastest way is to delete all objects and then the bucket in one command:
aws s3 rb s3://$BUCKET --force
s3 rb is "remove bucket"; --force deletes all objects inside before deleting the bucket.
If you want to make each step explicit:
# Delete all objects
aws s3 rm s3://$BUCKET --recursive
# Then delete the empty bucket
aws s3 rb s3://$BUCKET
Check the bucket is gone:
aws s3 ls
Clean up the temp directory on your machine too if you want:
cd .. && rm -rf site
Wrap-up
You just stored files on S3 and hosted a static website on the Internet without any server running continuously, and saw that everything on S3 is private by default until you deliberately open it. S3 will come back in later articles as a general storage location.
In Article 5 we get into databases: create PostgreSQL on RDS, connect from EC2, and apply the lesson from Article 3 — put the database in a private subnet rather than exposed to the Internet.