S3: Store Files and Host a Static Website

K
Kai··5 min read

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

  1. Create an S3 bucket.
  2. Upload files to the bucket via both the UI and the CLI.
  3. Enable static website hosting and open the site on the Internet.
  4. 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 test or my-bucket are certain to collide; give it a distinctive part, for example devops-series-<your-name>-2025.
  • An object is a file you store in a bucket, with a key (like a path). An object images/logo.png has the key images/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.