S3, R2 and MinIO
The s3 adapter uploads the build to a bucket with the S3 API. It works with AWS and with S3-compatible services through endpoint. Create the bucket first; Kilim does not create buckets or change their settings.
import { defineConfig } from "@kilim-dev/core";
import { s3 } from "@kilim-dev/core/adapters";
export default defineConfig({
url: "https://www.example.com",
deploy: s3({ bucket: "my-site", region: "eu-central-1" }),
});For Cloudflare R2, use the account's S3 address and region auto; for MinIO, the server address:
deploy: s3({
bucket: "my-site",
region: "auto",
endpoint: "https://<account-id>.r2.cloudflarestorage.com",
}),- Credentials come only from
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEYand, for temporary credentials,AWS_SESSION_TOKEN. The key needs to list, read, write and delete objects in the bucket (s3:ListBucket,s3:GetObject,s3:PutObject,s3:DeleteObject). For R2, create an R2 API token and use its S3 access key and secret. Never put them in the config. prefixputs the site in a folder of the bucket, e.g."blog/"; by default it goes to the root.- Without
endpoint, requests go tohttps://<bucket>.s3.<region>.amazonaws.com. Withendpointthe bucket is put in the path (<endpoint>/<bucket>/...), which MinIO and R2 expect; bucket names with dots use the path too.forcePathStyle: true | falseoverrides both. - Every deploy uploads
_kilim/deploy.jsonlast, the list of the site's files and their MD5 hashes. The nextkilim deployreads it and uploads only new and changed files. When the bucket has none (the first deploy, or a bucket filled by another tool), Kilim lists the objects underprefixand compares their ETags; files uploaded in several parts always count as changed. - Files the build no longer has are deleted, after the uploads. Without a manifest that means every object under
prefixthat is not in the build, so runkilim deploy --dry-runfirst on a bucket with other files, or setdeleteRemoved: falseto keep old files. Kept files stay in the manifest, so settingdeleteRemoved: truelater deletes them. - Pages are uploaded after stylesheets and images, so a visitor never gets a page whose assets are missing. Each file gets its
Content-Type(text types withcharset=utf-8) and aCache-Control: a year andimmutablefor the hashed files in_kilim/css/and_kilim/img/,max-age=0, must-revalidatefor everything else. - S3 does not read
_headersand_redirects, so they are not uploaded. Redirects fromaliasesandredirects.txtstill work through the redirect pages Kilim writes for them. For the security headers and CSP in_headers, add them where the site is served: a CloudFront response headers policy, or R2 custom domain rules. - To serve the bucket as a website, turn on static website hosting with
index.htmlas the index document and404.htmlas the error document, or put CloudFront (or an R2 custom domain) in front of it.
kilim doctor shows the address requests go to, whether the credentials are set, and warns about region: "auto" without endpoint or an endpoint that uses plain http.
# .github/workflows/s3.yml
name: S3
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx kilim deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}