Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
HTTP_PORT=80
# HTTPS_PORT=443 # uncomment after you enable TLS in caddy/Caddyfile

# ── Public domain & protocol ------------------------------------------
# Domain and protocol used by Puter and its subdomains (api, app, site,
# dev, host). Also used by s3-init to configure S3/RustFS bucket CORS
# for browser presigned uploads. Set PUTER_PROTOCOL=https when TLS is enabled.
PUTER_DOMAIN=puter.localhost
PUTER_PROTOCOL=http

# ── MariaDB ------------------------------------------------------------
MARIADB_ROOT_PASSWORD=replace-with-strong-password
MARIADB_DATABASE=puter
Expand Down
13 changes: 11 additions & 2 deletions doc/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker
| `puter-valkey` | `valkey/valkey:8-alpine` | Redis-compatible cache + rate-limiter |
| `puter-dynamo` | `amazon/dynamodb-local` | KV store — table auto-created on first boot |
| `puter-s3` | `rustfs/rustfs` | S3-compatible object storage (MinIO drop-in noted in file) |
| `puter-s3-init` | `amazon/aws-cli` | One-shot — creates the bucket on first boot, then exits |
| `puter-s3-init` | `amazon/aws-cli` | One-shot — creates the bucket and configures CORS, then exits |

Optional services (compose profile `ai`, opt-in):

Expand Down Expand Up @@ -54,6 +54,9 @@ cat > .env <<EOF
HTTP_PORT=80
# HTTPS_PORT=443 # uncomment after enabling TLS in Step 3

PUTER_DOMAIN=puter.localhost
PUTER_PROTOCOL=http

MARIADB_ROOT_PASSWORD=$MARIADB_ROOT_PASSWORD
MARIADB_DATABASE=puter
MARIADB_USER=puter
Expand Down Expand Up @@ -135,6 +138,12 @@ Replace `puter.localhost`, `site.puter.localhost`, `host.puter.localhost`, `dev.

Why these knobs:

- `PUTER_DOMAIN` and `PUTER_PROTOCOL` (in `.env`) — the domain and scheme (`http` or `https`) Puter and its subdomains (`api`, `app`, `site`, `dev`, `host`) serve on. Passed to `s3-init` in `docker-compose.yml` to automatically configure bucket-level CORS on RustFS (`s3.<domain>`). Direct browser uploads to presigned S3 URLs require CORS preflight (`OPTIONS`) approval from RustFS; without bucket CORS configuration, the browser blocks the upload. `s3-init` automatically configures CORS origins for `${PUTER_PROTOCOL}://${PUTER_DOMAIN}` and its required subdomains. For HTTPS deployments, set:
```bash
PUTER_DOMAIN=example.com
PUTER_PROTOCOL=https
```
This ensures CORS rules allow `https://...` origins rather than `http://...`. The `s3-init` service is idempotent: if the bucket already exists (e.g. upgrades or restarts), it detects the bucket and applies/updates the CORS policy rather than exiting early.
- `jwt_secret_v2` — the HMAC secret Puter signs and verifies auth tokens with (`kid: 'v2'` JWT header). The pre-v2 token format is retired: a token signed with the old `jwt_secret` no longer verifies, and holders are asked to sign in again. If you are upgrading from a release that had `jwt_secret`, drop it from your config — it is ignored.
- `env: "prod"` — the bundled `config.default.json` ships with `env: "dev"` (matches the source-tree `npm run start:gui` workflow, which expects webpack-dev-server emitting a CSS manifest). Self-host runs against pre-built static bundles, so `env: "prod"` makes the homepage emit the `/dist/bundle.min.css` `<link>` tag instead of waiting on a manifest that doesn't exist.
- `database.migrationPaths` — Puter applies the bundled MySQL/MariaDB schema on boot. The migration files are idempotent, so it is safe to leave this configured across restarts.
Expand Down Expand Up @@ -193,7 +202,7 @@ Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`.
1. Open [caddy/Caddyfile](../caddy/Caddyfile) and uncomment the `# :443 { … }` block at the bottom.
2. (Optional but recommended) Replace the plain `:80 { import puter_routes }` block with the `redir` version shown alongside it, to force HTTPS everywhere.
3. In [docker-compose.yml](../docker-compose.yml), uncomment the `443:443` port mapping under the `caddy` service.
4. In `.env`, uncomment `HTTPS_PORT=443`.
4. In `.env`, uncomment `HTTPS_PORT=443` and set `PUTER_PROTOCOL=https` (so `s3-init` generates `https://...` CORS origins).
5. In `config.json`, switch:
```json
{ "protocol": "https", "pub_port": 443 }
Expand Down
32 changes: 29 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ services:
start_period: 5s

s3-init:
# One-shot container that creates the `puter-local` bucket on first
# boot. Exits 0 once the bucket exists; stays exited 0 thereafter.
# One-shot container that creates the `puter-local` bucket and applies
# the browser CORS policy. Both operations are idempotent.
image: amazon/aws-cli:latest
container_name: puter-s3-init
depends_on:
Expand All @@ -150,11 +150,13 @@ services:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:-puter}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY:-puter-secret-change-me}
AWS_DEFAULT_REGION: us-east-1
PUTER_DOMAIN: ${PUTER_DOMAIN:-puter.localhost}
PUTER_PROTOCOL: ${PUTER_PROTOCOL:-http}
entrypoint:
- /bin/sh
- -c
- |
set -e
set -eu
endpoint=http://s3:9000
bucket=${S3_BUCKET:-puter-local}
if aws --endpoint-url "$$endpoint" s3api head-bucket --bucket "$$bucket" 2>/dev/null; then
Expand All @@ -163,6 +165,30 @@ services:
echo "creating bucket $$bucket"
aws --endpoint-url "$$endpoint" s3 mb "s3://$$bucket"
fi
cat > /tmp/cors.json <<EOF
{
"CORSRules": [
{
"AllowedOrigins": [
"$${PUTER_PROTOCOL}://$${PUTER_DOMAIN}",
"$${PUTER_PROTOCOL}://api.$${PUTER_DOMAIN}",
"$${PUTER_PROTOCOL}://app.$${PUTER_DOMAIN}",
"$${PUTER_PROTOCOL}://site.$${PUTER_DOMAIN}",
"$${PUTER_PROTOCOL}://dev.$${PUTER_DOMAIN}",
"$${PUTER_PROTOCOL}://host.$${PUTER_DOMAIN}"
],
"AllowedMethods": ["GET", "HEAD", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "x-amz-request-id"],
"MaxAgeSeconds": 3600
}
]
}
EOF
aws --endpoint-url "$$endpoint" s3api put-bucket-cors \
--bucket "$$bucket" \
--cors-configuration file:///tmp/cors.json
echo "bucket $$bucket CORS configured"
restart: "no"

# ── Optional: local LLM ───────────────────────────────────────────
Expand Down
30 changes: 17 additions & 13 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,21 @@
# compose file + brings the stack up. Set PUTER_FORCE=1 to overwrite.
#
# Tunable env vars (or pass as -Parameters when running the file directly):
# PUTER_DIR install directory (default: ./puter-selfhosted)
# PUTER_URL base URL to fetch docker-compose.yml (default: GitHub raw, main branch)
# PUTER_DOMAIN domain Puter will serve on (default: puter.localhost)
# PUTER_PORT HTTP port for Caddy (default: 80)
# PUTER_FORCE set to 1 to overwrite existing .env / config.json
# PUTER_DIR install directory (default: ./puter-selfhosted)
# PUTER_URL base URL to fetch docker-compose.yml (default: GitHub raw, main branch)
# PUTER_DOMAIN domain Puter will serve on (default: puter.localhost)
# PUTER_PORT HTTP port for Caddy (default: 80)
# PUTER_PROTOCOL public scheme: http | https (default: http)
# PUTER_FORCE set to 1 to overwrite existing .env / config.json

[CmdletBinding()]
param(
[string]$PuterDir = $(if ($env:PUTER_DIR) { $env:PUTER_DIR } else { 'puter-selfhosted' }),
[string]$PuterUrl = $(if ($env:PUTER_URL) { $env:PUTER_URL } else { 'https://raw.githubusercontent.com/HeyPuter/puter/main' }),
[string]$PuterDomain = $(if ($env:PUTER_DOMAIN) { $env:PUTER_DOMAIN } else { 'puter.localhost' }),
[int] $PuterPort = $(if ($env:PUTER_PORT) { [int]$env:PUTER_PORT } else { 80 }),
[switch]$Force = $($env:PUTER_FORCE -eq '1')
[string]$PuterDir = $(if ($env:PUTER_DIR) { $env:PUTER_DIR } else { 'puter-selfhosted' }),
[string]$PuterUrl = $(if ($env:PUTER_URL) { $env:PUTER_URL } else { 'https://raw.githubusercontent.com/HeyPuter/puter/main' }),
[string]$PuterDomain = $(if ($env:PUTER_DOMAIN) { $env:PUTER_DOMAIN } else { 'puter.localhost' }),
[int] $PuterPort = $(if ($env:PUTER_PORT) { [int]$env:PUTER_PORT } else { 80 }),
[string]$PuterProtocol = $(if ($env:PUTER_PROTOCOL) { $env:PUTER_PROTOCOL } else { 'http' }),
[switch]$Force = $($env:PUTER_FORCE -eq '1')
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -125,6 +127,8 @@ if ($writeConfig) {
HTTP_PORT=$PuterPort
# HTTPS_PORT=443 # uncomment after enabling TLS in caddy/Caddyfile
# # (see "Step 3 — TLS" in doc/self-hosting.md)
PUTER_DOMAIN=$PuterDomain
PUTER_PROTOCOL=$PuterProtocol

MARIADB_ROOT_PASSWORD=$mariadbRootPw
MARIADB_DATABASE=puter
Expand All @@ -140,7 +144,7 @@ S3_BUCKET=puter-local
Write-Log 'writing puter/config/config.json'
$config = [ordered]@{
domain = $PuterDomain
protocol = 'http'
protocol = $PuterProtocol
pub_port = $PuterPort
env = 'prod'
static_hosting_domain = "site.$PuterDomain"
Expand Down Expand Up @@ -178,7 +182,7 @@ S3_BUCKET=puter-local
s3 = [ordered]@{
s3Config = [ordered]@{
endpoint = 'http://s3:9000'
publicEndpoint = "http://s3.$PuterDomain"
publicEndpoint = "${PuterProtocol}://s3.$PuterDomain"
accessKeyId = 'puter'
secretAccessKey = $s3SecretKey
region = 'us-east-1'
Expand Down Expand Up @@ -206,6 +210,6 @@ Write-Log 'stack starting. first boot takes ~30s while MariaDB initialises.'
Write-Log 'follow puter logs:'
Write-Log " cd $PuterDir; docker compose logs -f puter"
Write-Log ''
Write-Log "open http://${PuterDomain}:${PuterPort} once the puter container is healthy."
Write-Log "open ${PuterProtocol}://${PuterDomain}:${PuterPort} once the puter container is healthy."
Write-Log 'first-boot admin password is logged once — grab it with:'
Write-Log " cd $PuterDir; docker compose logs puter | Select-String password"
2 changes: 2 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ if [ "$write_config" = "1" ]; then
HTTP_PORT=$PUTER_PORT
# HTTPS_PORT=443 # uncomment after enabling TLS in caddy/Caddyfile
# # (see "Step 3 — TLS" in doc/self-hosting.md)
PUTER_DOMAIN=$PUTER_DOMAIN
PUTER_PROTOCOL=$PUTER_PROTOCOL

MARIADB_ROOT_PASSWORD=$MARIADB_ROOT_PASSWORD
MARIADB_DATABASE=puter
Expand Down
Loading