Skip to content

Static Website Hosting

Ben McClelland edited this page Sep 18, 2026 · 2 revisions

VersityGW can expose bucket objects as static websites through a dedicated HTTP or HTTPS listener. Website configuration is managed through the normal, authenticated S3 API, while browsers access content anonymously through the website endpoint.

The website endpoint supports:

  • GET, HEAD, and CORS preflight OPTIONS requests.
  • Index and custom error documents.
  • Whole-site, routing-rule, and per-object redirects.
  • Virtual-host routing for multiple website buckets.
  • Object HTTP headers, user metadata, and bucket CORS rules.

It is not an alternate S3 API endpoint. Uploads, bucket administration, and signed S3 requests continue to use the main --port listener.

Start the website endpoint

The following example runs the S3 API on port 7070 and website hosting on port 8080:

versitygw \
  --port :7070 \
  --website :8080 \
  --website-domain example.com \
  [other global options] \
  posix /srv/versitygw
Option Environment variable Purpose
--website <address> VGW_WEBSITE_PORT Enables the website listener. It accepts the same TCP and UNIX-socket address forms as --port and may be specified more than once.
--website-domain <domain> VGW_WEBSITE_DOMAIN Selects the base domain used to map request hosts to bucket names.
--website-cert <file> VGW_WEBSITE_CERT TLS certificate for the website listener.
--website-key <file> VGW_WEBSITE_KEY TLS private key for the website listener.
--website-no-tls VGW_WEBSITE_NO_TLS Forces plaintext HTTP even when the main gateway has TLS configured.

If website-specific certificate files are omitted, the listener inherits the main --cert and --key files. The certificate and key must always be configured as a pair. SIGHUP reloads website certificates along with the other gateway certificates.

The website and S3 API listeners must use different addresses. Website traffic normally uses ports 80 or 443 through a reverse proxy or load balancer, while the S3 API remains on its own endpoint.

Host and bucket mapping

With --website-domain example.com, VersityGW maps hosts as follows:

Request host Bucket
blog.example.com blog
docs.example.com docs
example.com example.com

Only one label may precede the configured domain. For example, www.blog.example.com does not resolve to a bucket. Configure wildcard DNS such as *.example.com for subdomain buckets and an apex record for example.com if the apex bucket will be used.

For HTTPS, the certificate must cover the names clients use. A wildcard certificate for *.example.com does not normally cover the apex example.com, so include both names when serving both forms.

When --website-domain is omitted, the listener runs in catch-all mode and treats the complete request hostname as the bucket name:

Request host Bucket
blog.example.com blog.example.com
mysite.example.net mysite.example.net

Catch-all mode is useful for buckets named after complete custom domains. Although the CLI help describes --website-domain as required, an omitted domain is accepted and logged as initializing website endpoint (catch-all).

Configure a website

The examples below assume:

  • S3 API endpoint: https://s3.example.com
  • Website endpoint: http://blog.example.com:8080
  • Bucket: blog

Create the bucket and upload the site through the authenticated S3 endpoint:

aws --endpoint-url https://s3.example.com s3 mb s3://blog
aws --endpoint-url https://s3.example.com s3 cp ./site/ s3://blog/ \
  --recursive

Set appropriate content types while uploading. The website endpoint returns the stored Content-Type, Cache-Control, Content-Encoding, Content-Language, Content-Disposition, Expires, ETag, and Last-Modified headers; it does not infer or rewrite them when serving the object.

Grant public read access

Website requests do not carry S3 credentials. Objects must therefore allow anonymous s3:GetObject access through a bucket policy or ACL. A minimal bucket policy is:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicWebsiteRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::blog/*"
    }
  ]
}

Save it as public-read.json, then apply it through the S3 endpoint:

aws --endpoint-url https://s3.example.com s3api put-bucket-policy \
  --bucket blog \
  --policy file://public-read.json

Configuring a website does not make a bucket public. If public object access is missing, the website endpoint returns 403 AccessDenied. The configured error document is also fetched as an anonymous object and cannot be served unless it is public.

Set index and error documents

Create website.json:

{
  "IndexDocument": {
    "Suffix": "index.html"
  },
  "ErrorDocument": {
    "Key": "error.html"
  }
}

Apply it:

aws --endpoint-url https://s3.example.com s3api put-bucket-website \
  --bucket blog \
  --website-configuration file://website.json

The index suffix is appended only to the site root and paths ending in /:

Website request Object read
/ index.html
/docs/ docs/index.html
/docs docs

The suffix must be non-empty and cannot contain /. If an object read fails and ErrorDocument is configured, the gateway serves that object's body and headers while retaining the original error status, such as 404. If the error document is absent or private, its own error response is returned.

Verify the website without changing local DNS:

curl --resolve blog.example.com:8080:127.0.0.1 \
  http://blog.example.com:8080/

Redirects

VersityGW implements the S3 website redirect forms in this order:

  1. RedirectAllRequestsTo
  2. A matching prefix-only routing rule
  3. An object's x-amz-website-redirect-location
  4. A routing rule matching an object-read error
  5. The configured error document

Redirect targets preserve the original query string. Keys are URL-encoded in the generated Location header.

Redirect an entire website

RedirectAllRequestsTo is mutually exclusive with index, error, and routing rule configuration:

{
  "RedirectAllRequestsTo": {
    "HostName": "www.example.com",
    "Protocol": "https"
  }
}

Every request receives a 301 redirect. Object public access is not required because no object is read.

Add routing rules

The following configuration moves a prefix before reading an object and redirects remaining 404 responses:

{
  "IndexDocument": {
    "Suffix": "index.html"
  },
  "ErrorDocument": {
    "Key": "error.html"
  },
  "RoutingRules": [
    {
      "Condition": {
        "KeyPrefixEquals": "old-docs/"
      },
      "Redirect": {
        "ReplaceKeyPrefixWith": "docs/",
        "HttpRedirectCode": "301"
      }
    },
    {
      "Condition": {
        "HttpErrorCodeReturnedEquals": "404"
      },
      "Redirect": {
        "HostName": "fallback.example.com",
        "ReplaceKeyWith": "not-found",
        "Protocol": "https",
        "HttpRedirectCode": "302"
      }
    }
  ]
}

Rules are evaluated in configuration order. A condition may contain KeyPrefixEquals, HttpErrorCodeReturnedEquals, or both. A rule with only a key prefix is applied before object access is checked; a rule with an error code is evaluated after the object read fails. Configuration is limited to 50 routing rules and 128 KiB.

ReplaceKeyWith and ReplaceKeyPrefixWith cannot appear in the same redirect. An omitted redirect code defaults to 301, and the supported explicit codes are 301, 302, 303, 304, 305, 307, and 308. Redirect protocols are limited to http and https.

Redirect one object

Set x-amz-website-redirect-location while uploading an object:

aws --endpoint-url https://s3.example.com s3api put-object \
  --bucket blog \
  --key old-page.html \
  --website-redirect-location /new-page.html \
  --body old-page.html

A successful website lookup of old-page.html returns a 301 with the stored location instead of the object body. The object must still be publicly readable before its redirect metadata can be evaluated.

CORS

Website GET and HEAD responses use the bucket's S3 CORS configuration. The website endpoint also handles browser preflight OPTIONS requests. Configure CORS with PutBucketCors when site content must be fetched by scripts running on another origin. Without a matching bucket CORS rule, a preflight request is rejected.

POST, PUT, PATCH, and DELETE are not supported on the website listener and return 405 MethodNotAllowed with:

Allow: GET, HEAD, OPTIONS

Inspect or remove configuration

Use the authenticated S3 endpoint:

aws --endpoint-url https://s3.example.com s3api get-bucket-website \
  --bucket blog

aws --endpoint-url https://s3.example.com s3api delete-bucket-website \
  --bucket blog

Deleting the configuration leaves the bucket and objects unchanged. Website requests then return NoSuchWebsiteConfiguration.

Backend support

Website configuration and object serving are supported by the built-in POSIX, ScoutFS, Azure Blob, and S3 proxy backends. A plugin backend must implement PutBucketWebsite, GetBucketWebsite, and DeleteBucketWebsite, in addition to public object reads, to support this endpoint.

Related documentation

Clone this wiki locally