Builds
Nginx reverse proxy to Hetzner Object Storage with local caching. Upload builds with s3cmd, testers access them at https://builds.kill.systems/<version>/.
Setup
1. Create a public bucket for builds
In Hetzner Console → Object Storage, create a new bucket (e.g. kill-builds). Set visibility to public so Nginx can proxy to it without authentication.
Generate S3 credentials if you haven't already (Security → S3 Credentials).
2. Configure s3cmd locally
# Install
brew install s3cmd # or apt install s3cmd
# Configure
s3cmd --configure \
--host=fsn1.your-objectstorage.com \
--host-bucket='%(bucket)s.fsn1.your-objectstorage.com'
Enter your Hetzner S3 access key and secret key when prompted.
3. Push this repo to Gitea
cd ksys-builds
git init
git add .
git commit -m "game builds proxy"
git remote add origin https://src.kill.systems/<user>/builds.git
git push -u origin main
4. Deploy in Dokploy
- Create a new project (e.g. "builds")
- Add a new Application service (not Docker Compose)
- Set source to your Gitea repo
- In Environment Variables, add:
BUCKET_ORIGIN=https://ksys-builds.fsn1.your-objectstorage.com/builds/ BUCKET_HOST=ksys-builds.fsn1.your-objectstorage.com - Set the port to
3000 - Deploy
5. Add the domain
In the Domains tab, add builds.kill.systems with HTTPS and Let's Encrypt on port 3000.
DNS at Squarespace:
Host: builds
Type: A
Data: 46.224.133.129
Uploading Builds
Edit deploy.sh and set your bucket name, then:
# Upload current build (uses git hash as version)
./deploy.sh ./dist
# Upload with a specific version name
./deploy.sh ./dist v1.2.3
# Upload with a custom label
./deploy.sh ./dist beta-march15
Testers visit https://builds.kill.systems/<version>/ to play.
How it works
Tester → builds.kill.systems → Nginx (cache) → Hetzner Object Storage
↓
Cached for 30 days
(builds are immutable)
First request fetches from Object Storage and caches locally. Subsequent requests are served from the Nginx cache. The X-Cache-Status response header shows HIT or MISS.
Since each build has a unique path (the git hash), cache invalidation is never needed — new builds go to new paths.