From 7cf3e034335dd38fcdfdffba5db4dafc8dbd83c3 Mon Sep 17 00:00:00 2001 From: Sauhbagya Prasad Date: Wed, 29 Jul 2026 03:22:13 +0530 Subject: [PATCH] Replaced Nginx with Caddy (#3378) * Replace nginx with Caddy * updated caddy location * seprate logic for local and with domain caddy setup * updated install * updated install scripts new ps1 script with caddy, and removed instances of nginx * fix Caddy Host routing, restore TLS/SELinux/healthcheck, update docs --------- Co-authored-by: Daniel Salazar --- .env.example | 4 +- caddy/Caddyfile | 73 ++++++++++++++++++++++++ doc/self-hosting.md | 36 ++++++------ docker-compose.yml | 34 +++++++---- install.ps1 | 27 +++++---- install.sh | 54 ++++++++++++------ nginx/nginx.conf | 135 -------------------------------------------- 7 files changed, 168 insertions(+), 195 deletions(-) create mode 100644 caddy/Caddyfile delete mode 100644 nginx/nginx.conf diff --git a/.env.example b/.env.example index b53caa16d..726f74601 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,9 @@ # docker-compose.full.yml up -d`. None of the defaults below are safe for # anything beyond a local laptop test. -# ── Public-facing ports (nginx) --------------------------------------- +# ── Public-facing ports (Caddy) --------------------------------------- HTTP_PORT=80 -# HTTPS_PORT=443 # uncomment after you enable TLS in nginx/nginx.conf +# HTTPS_PORT=443 # uncomment after you enable TLS in caddy/Caddyfile # ── MariaDB ------------------------------------------------------------ MARIADB_ROOT_PASSWORD=replace-with-strong-password diff --git a/caddy/Caddyfile b/caddy/Caddyfile new file mode 100644 index 000000000..90454fc0d --- /dev/null +++ b/caddy/Caddyfile @@ -0,0 +1,73 @@ +# Reverse proxy in front of Puter — mirrors what the prod ALB does: +# accepts every Host header, forwards to the Puter container, and lets +# the Puter app handle subdomain-based routing internally (api.*, +# site.*, app.*, dev.*, plus the per-user subdomains under those). +# +# To enable TLS: +# 1. Drop a wildcard fullchain.pem + privkey.pem into ./puter/tls/ +# (see "Step 3 — TLS" in doc/self-hosting.md). +# 2. Uncomment the `:443` block at the bottom of this file, and swap the +# `:80` block for the redirect shown alongside it. +# 3. Uncomment the `443:443` port mapping under the `caddy` service in +# docker-compose.yml, and HTTPS_PORT in .env. +# 4. Set `"protocol": "https"` and `"pub_port": 443` in config.json. +{ + # Certs are supplied by the operator, not issued by Caddy. Puter serves + # per-user sites and apps on dynamic subdomains (.site., + # .app.) — only a DNS-01 wildcard cert covers those, and + # DNS-01 needs a provider plugin that isn't in the stock caddy image. + # `off` stops Caddy attempting ACME on boot (it would fail and leave the + # site unreachable) and stops it inventing its own http→https redirects. + auto_https off +} + +# Shared handling, imported by the HTTP and HTTPS site blocks below so the +# two can't drift apart. +(puter_routes) { + # Rough size cap that mirrors prod ALB defaults; tune for your uploads. + # Puter chunks large uploads, so 1 GiB per request is plenty. + request_body { + max_size 1024MiB + } + + # RustFS — see the `s3` service in docker-compose.yml. Browsers PUT/GET + # here for presigned-URL uploads / downloads. Matched on the `s3.` + # subdomain of whatever the install domain is, so signature verification + # works (Caddy preserves the original Host end-to-end) and HTTPS stays + # clean — no mixed content from a port-9000 host publish. + @s3 header_regexp Host ^s3\. + handle @s3 { + reverse_proxy s3:9000 + } + + # Everything else, on every Host, goes to Puter — which routes on that + # Host internally. Caddy forwards it unchanged and adds X-Forwarded-For + # / -Proto / -Host, which is what `trust_proxy` in config.json counts. + handle { + reverse_proxy puter:4100 { + # Stream responses through unbuffered — Puter uses SSE and + # socket.io. WebSocket upgrades need no config of their own; + # Caddy proxies them by default. + flush_interval -1 + } + } +} + +# ── HTTP (port 80) ───────────────────────────────────────────────────── +# A site address with no hostname is the catch-all: it answers for every +# Host, which is what Puter's subdomain routing requires. +:80 { + import puter_routes +} + +# ── HTTPS (port 443) — uncomment after dropping certs in ./puter/tls/ ── +# Replace the `:80` block above with a redirect to force HTTPS everywhere: +# +# :80 { +# redir https://{host}{uri} permanent +# } +# +# :443 { +# tls /etc/caddy/tls/fullchain.pem /etc/caddy/tls/privkey.pem +# import puter_routes +# } diff --git a/doc/self-hosting.md b/doc/self-hosting.md index e6ef1d29a..84e3afd1c 100644 --- a/doc/self-hosting.md +++ b/doc/self-hosting.md @@ -1,6 +1,6 @@ # Self-Hosting Puter -`docker-compose.yml` brings up Puter **plus every external service it needs** — MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx — wired together. Closest thing to a production deployment you can self-manage on a single host. +`docker-compose.yml` brings up Puter **plus every external service it needs** — MariaDB, Valkey, DynamoDB-local, RustFS S3, Caddy — wired together. Closest thing to a production deployment you can self-manage on a single host. ## One-line installer (recommended) @@ -8,7 +8,7 @@ curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh ``` -Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker-compose.yml` from the OSS repo, and runs `docker compose up -d`. Re-running is safe — it won't overwrite existing config (set `PUTER_FORCE=1` to rotate). Use the manual steps below if you want to inspect or tweak each step yourself. +Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker-compose.yml` + `caddy/Caddyfile` from the OSS repo, and runs `docker compose up -d`. Re-running is safe — it won't overwrite existing config (set `PUTER_FORCE=1` to rotate). Use the manual steps below if you want to inspect or tweak each step yourself. ## Requirements @@ -20,7 +20,7 @@ Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker | Container | Image | Role | | --------------- | ------------------------ | ---------------------------------------------------------- | -| `puter-nginx` | `nginx:1.27-alpine` | Reverse proxy on 80 (and 443 if TLS); forwards to Puter | +| `puter-caddy` | `caddy:2.11-alpine` | Reverse proxy on 80 (and 443 if TLS); forwards to Puter | | `puter` | `ghcr.io/heyputer/puter` | The app | | `puter-mariadb` | `mariadb:11` | SQL database — schema applied automatically on first boot | | `puter-valkey` | `valkey/valkey:8-alpine` | Redis-compatible cache + rate-limiter | @@ -65,7 +65,7 @@ S3_SECRET_KEY=$S3_SECRET_KEY S3_BUCKET=puter-local EOF -mkdir -p puter/config puter/data puter/tls +mkdir -p puter/config puter/data puter/data/caddy puter/tls cat > puter/config/config.json </`). Real AWS S3 wants virtual-hosted (`.`) — drop this flag (or set `false`) when you swap to real S3. -- `s3.s3Config.publicEndpoint` — `endpoint` (`http://s3:9000`) only resolves inside the docker network; presigned upload/download URLs handed to the browser need a host-reachable URL. nginx routes the `s3.` subdomain to RustFS internally and preserves the Host header end-to-end (required for S3 signature validation), so the browser hits the same port/protocol as the rest of the app — no separate published port, no mixed-content surprises when you turn on TLS. Switch to `https://s3.` once you enable TLS in Step 3. Real AWS S3 doesn't need this — its endpoint is already public; drop the field entirely. -- `trust_proxy: 1` — nginx terminates TLS and forwards `X-Forwarded-For`. Without this, `req.ip` is the docker-network address of the nginx container instead of the real client IP, which breaks rate limiting and IP-based audit logs. `1` = one trusted hop (nginx). Bump to `2` if you put Cloudflare in front of nginx; never set `true` (it trusts every hop and makes XFF forgeable). +- `s3.s3Config.publicEndpoint` — `endpoint` (`http://s3:9000`) only resolves inside the docker network; presigned upload/download URLs handed to the browser need a host-reachable URL. Caddy routes the `s3.` subdomain to RustFS internally and preserves the Host header end-to-end (required for S3 signature validation), so the browser hits the same port/protocol as the rest of the app — no separate published port, no mixed-content surprises when you turn on TLS. Switch to `https://s3.` once you enable TLS in Step 3. Real AWS S3 doesn't need this — its endpoint is already public; drop the field entirely. +- `trust_proxy: 1` — Caddy terminates TLS and forwards `X-Forwarded-For`. Without this, `req.ip` is the docker-network address of the Caddy container instead of the real client IP, which breaks rate limiting and IP-based audit logs. `1` = one trusted hop (Caddy). Bump to `2` if you put Cloudflare in front of Caddy; never set `true` (it trusts every hop and makes XFF forgeable). > If you ever change `MARIADB_PASSWORD` after first boot, `.env` alone won't update MariaDB — its credentials are baked into `./puter/data/mariadb/` on first init. Either rotate the password inside MariaDB by hand or `docker compose down && rm -rf ./puter/data/mariadb` to start fresh. ## Step 2 — Point DNS at the server \[Optional\] -In your DNS provider, add records for the main domain plus the subdomains Puter and nginx route on (`api.*`, `site.*`, `app.*`, `s3.*`): +In your DNS provider, add records for the main domain plus the subdomains Puter and Caddy route on (`api.*`, `site.*`, `app.*`, `s3.*`): ``` A puter.localhost → @@ -163,7 +163,7 @@ A dev.puter.localhost → A *.dev.puter.localhost → ``` -The wildcards are required — Puter routes via subdomains (`api.*`, `app.*`, etc.) and nginx routes browser S3 traffic via `s3.*` to RustFS. +The wildcards are required — Puter routes via subdomains (`api.*`, `app.*`, etc.) and Caddy routes browser S3 traffic via `s3.*` to RustFS. ## Step 3 — TLS (recommended for public installs) \[Optional\] @@ -182,13 +182,15 @@ sudo certbot certonly --manual --preferred-challenges dns \ The cert needs to cover `*.puter.localhost` so that `s3.puter.localhost` (browser S3 endpoint), plus Puter's own `api.*` / `app.*` subdomains, all validate. +> **Why not Caddy's automatic HTTPS?** Caddy issues certs over ACME by itself, but only for hostnames it knows up front, and only over HTTP-01 with the stock image. Puter serves every user's site and app on a subdomain it invents at runtime (`.site.`, `.app.`), which needs a **wildcard** cert — and wildcards require DNS-01, which requires a DNS-provider plugin that isn't in `caddy:2.11-alpine`. So `caddy/Caddyfile` sets `auto_https off` and reads the cert you supply below. If you'd rather have Caddy manage certs, build an image with the plugin for your DNS provider (`xcaddy build --with github.com/caddy-dns/`) and swap the `tls` line for a `tls { dns … }` block. + Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`. -**Wire nginx to use them:** +**Wire Caddy to use them:** -1. Open [nginx/nginx.conf](../nginx/nginx.conf), uncomment **both** `# server { listen 443 ssl … }` blocks (one for `s3.*`, one for the catch-all). -2. (Optional) Replace the body of the port-80 blocks with `return 301 https://$host$request_uri;` to force HTTPS everywhere. -3. In [docker-compose.yml](../docker-compose.yml), uncomment the `443:443` port mapping under the `nginx` service. +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`. 5. In `config.json`, switch: ```json @@ -201,9 +203,9 @@ Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`. ## Running behind your own reverse proxy -The bundled `puter-nginx` mirrors production and is the supported default — it terminates TLS (Step 3) and forwards every Host to Puter. But plenty of self-hosters already run their own edge proxy (Caddy, Traefik, HAProxy, a cloud load balancer, another nginx). You can put Puter behind it instead; Puter doesn't terminate TLS itself in any setup, so "your proxy does TLS" is just a matter of pointing it at the Puter container and getting a handful of details right. +The bundled `puter-caddy` mirrors production and is the supported default — it terminates TLS (Step 3) and forwards every Host to Puter. But plenty of self-hosters already run their own edge proxy (Traefik, nginx, HAProxy, a cloud load balancer, another Caddy). You can put Puter behind it instead; Puter doesn't terminate TLS itself in any setup, so "your proxy does TLS" is just a matter of pointing it at the Puter container and getting a handful of details right. -You can keep the bundled nginx as the single hop your proxy talks to, or bypass it and forward straight to the Puter container on port `4100` (uncomment the `4100:4100` mapping under the `puter` service in [docker-compose.yml](../docker-compose.yml), or attach your proxy to the compose network). Either works — what matters is the rules below. +You can keep the bundled Caddy as the single hop your proxy talks to, or bypass it and forward straight to the Puter container on port `4100` (uncomment the `4100:4100` mapping under the `puter` service in [docker-compose.yml](../docker-compose.yml), or attach your proxy to the compose network). Either works — what matters is the rules below. **The rules that actually matter** (getting any of these wrong is what causes the redirect loops and "Invalid Host header" failures people hit): @@ -219,7 +221,7 @@ You can keep the bundled nginx as the single hop your proxy talks to, or bypass - `X-Forwarded-For` — the real client IP (rate limiting + audit logs). - `Upgrade` / `Connection` — passed through for WebSocket / socket.io upgrades, or the realtime connection silently fails. -5. **Set `trust_proxy` to the number of hops in front of Puter.** One external proxy talking directly to Puter → `"trust_proxy": 1` (installer: `PUTER_TRUST_PROXY=1`). A second proxy in front (e.g. Cloudflare → your proxy → Puter) → `2`. This is the count of proxies between the client and Puter, **including** the bundled nginx if you keep it. Too low and `req.ip` becomes a proxy address (breaks rate limiting); never set `true` (it trusts every hop and makes `X-Forwarded-For` forgeable). +5. **Set `trust_proxy` to the number of hops in front of Puter.** One external proxy talking directly to Puter → `"trust_proxy": 1` (installer: `PUTER_TRUST_PROXY=1`). A second proxy in front (e.g. Cloudflare → your proxy → Puter) → `2`. This is the count of proxies between the client and Puter, **including** the bundled Caddy if you keep it. Too low and `req.ip` becomes a proxy address (breaks rate limiting); never set `true` (it trusts every hop and makes `X-Forwarded-For` forgeable). 6. **Route the wildcard to Puter.** Your proxy must forward `*.` (covering `api.*`, `app.*`, and `s3.`) and `*.site.` (and any other hosting domains) to Puter — same wildcard DNS as Step 2. Puter does the per-subdomain routing internally; the proxy just needs to hand it the traffic with the Host intact. @@ -227,6 +229,8 @@ With those in place the rest of the stack is unchanged — `docker compose up -d ## Step 4 — Bring it up +Both [docker-compose.yml](../docker-compose.yml) and [caddy/Caddyfile](../caddy/Caddyfile) need to sit next to your `.env` — clone the repo, or copy those two paths out of it. The Caddyfile is bind-mounted read-only; if it's missing, Docker creates a *directory* at that path and `puter-caddy` dies with "not a directory". + ```bash docker compose up -d ``` @@ -469,7 +473,7 @@ Migrations re-apply idempotently across pulls. Volumes are preserved. ## Troubleshooting -**Site loads but I get "Bad Gateway" / nginx errors.** +**Site loads but I get a 502 / "Bad Gateway" from Caddy.** The puter container failed to come up. `docker compose logs puter` will tell you which dependency rejected it (most often DB password mismatch between `.env` and `config.json`). **Login screen says "admin password not set".** diff --git a/docker-compose.yml b/docker-compose.yml index e3955bd02..344555bfb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ # Self-hosted Puter — full stack. # # Brings up Puter + every external service it needs: -# - nginx : reverse proxy (mirrors prod ALB; handles TLS + Host fan-out) +# - caddy : reverse proxy (mirrors prod ALB; handles TLS + Host fan-out) # - valkey : redis-compatible cache / rate-limiter backend # - mariadb : SQL database (Puter applies its schema on first boot) # - dynamo : DynamoDB-local (KV store; Puter creates the table itself) @@ -23,7 +23,8 @@ # # Production: # - Always replace the default passwords / S3 keys / Puter secrets. -# - Front Puter with TLS-terminating reverse proxy (Caddy / nginx). +# - Enable TLS on the bundled Caddy (see caddy/Caddyfile) or front the +# stack with your own TLS-terminating proxy. # - Move state-bearing volumes to a backed-up location. services: @@ -118,7 +119,7 @@ services: RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-puter-secret-change-me} volumes: - ./puter/data/s3:/data:z - # Internal-only — browsers reach RustFS via nginx (`s3.`), + # Internal-only — browsers reach RustFS via Caddy (`s3.`), # which preserves the Host header for S3 signature validation and # rides the same TLS termination as Puter. Uncomment to also expose # 9000 directly on the host for `aws-cli` / debugging. @@ -249,7 +250,7 @@ services: condition: service_started s3-init: condition: service_completed_successfully - # Internal-only: nginx reaches it on the compose network. Uncomment + # Internal-only: Caddy reaches it on the compose network. Uncomment # to also expose port 4100 directly on the host (useful for debugging). # ports: # - "4100:4100" @@ -270,23 +271,32 @@ services: retries: 3 start_period: 30s - nginx: - image: nginx:1.27-alpine - container_name: puter-nginx + caddy: + image: caddy:2.11-alpine + container_name: puter-caddy restart: unless-stopped depends_on: puter: condition: service_started ports: - "${HTTP_PORT:-80}:80" - # Uncomment when you enable TLS in nginx/nginx.conf: + # Uncomment when you enable TLS in caddy/Caddyfile: # - "${HTTPS_PORT:-443}:443" volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro,z - # TLS certs (fullchain.pem + privkey.pem). Read-only inside. - - ./puter/tls:/etc/nginx/tls:ro,z + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro,z + # TLS certs (wildcard fullchain.pem + privkey.pem). Read-only inside. + - ./puter/tls:/etc/caddy/tls:ro,z + # Caddy's own state — the cert store it would use if you swap this + # config over to ACME. Kept under ./puter/data with everything else + # so one directory is the whole backup. + - ./puter/data/caddy:/data:z healthcheck: - test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://localhost/ || exit 1"] + # Hits Caddy's local-only admin API rather than proxying through to + # Puter: this reports whether the proxy itself is up and configured, + # and keeps working once the `:80` block becomes an HTTPS redirect. + # 127.0.0.1, not localhost — the admin endpoint is IPv4-only and + # busybox wget tries ::1 first. + test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://127.0.0.1:2019/config/ >/dev/null || exit 1"] interval: 10s timeout: 3s retries: 5 diff --git a/install.ps1 b/install.ps1 index fbafc4da3..f45378092 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ # What this does, in order: # 1. Checks that docker (with the compose plugin) exists. # 2. Creates ./puter-selfhosted/ (override with $env:PUTER_DIR). -# 3. Downloads docker-compose.yml + nginx.conf from the OSS repo. +# 3. Downloads docker-compose.yml + caddy/Caddyfile from the OSS repo. # 4. Generates fresh secrets and writes .env + puter/config/config.json. # 5. Runs `docker compose up -d` and prints how to find the admin password. # @@ -23,7 +23,7 @@ # 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 nginx (default: 80) +# PUTER_PORT HTTP port for Caddy (default: 80) # PUTER_FORCE set to 1 to overwrite existing .env / config.json [CmdletBinding()] @@ -77,10 +77,10 @@ if ($LASTEXITCODE -ne 0) { # ── Step 2: install dir ───────────────────────────────────────────── $null = New-Item -ItemType Directory -Force -Path $PuterDir Set-Location $PuterDir -$null = New-Item -ItemType Directory -Force -Path 'puter/config', 'puter/data', 'puter/tls' +$null = New-Item -ItemType Directory -Force -Path 'puter/config', 'puter/data', 'puter/data/caddy', 'puter/tls' Write-Log "install dir: $((Get-Location).Path)" -# ── Step 3: docker-compose.yml + nginx config ────────────────────── +# ── Step 3: docker-compose.yml + Caddy config ────────────────────── Write-Log "downloading docker-compose.yml from $PuterUrl" try { Invoke-WebRequest -Uri "$PuterUrl/docker-compose.yml" -OutFile 'docker-compose.yml' -UseBasicParsing @@ -88,17 +88,19 @@ try { Die "could not fetch $PuterUrl/docker-compose.yml — $_" } -Write-Log "downloading nginx/nginx.conf from $PuterUrl" -$null = New-Item -ItemType Directory -Force -Path 'nginx' +# The Caddyfile is domain-agnostic — it answers on every Host and leaves +# the subdomain routing to Puter — so there's nothing to template in. +Write-Log "downloading caddy/Caddyfile from $PuterUrl" +$null = New-Item -ItemType Directory -Force -Path 'caddy' # If the path was previously auto-created as a directory by a failed # `compose up`, remove it so we can write the file there. -if (Test-Path 'nginx/nginx.conf' -PathType Container) { - Remove-Item 'nginx/nginx.conf' -Recurse -Force +if (Test-Path 'caddy/Caddyfile' -PathType Container) { + Remove-Item 'caddy/Caddyfile' -Recurse -Force } try { - Invoke-WebRequest -Uri "$PuterUrl/nginx/nginx.conf" -OutFile 'nginx/nginx.conf' -UseBasicParsing + Invoke-WebRequest -Uri "$PuterUrl/caddy/Caddyfile" -OutFile 'caddy/Caddyfile' -UseBasicParsing } catch { - Die "could not fetch $PuterUrl/nginx/nginx.conf — $_" + Die "could not fetch $PuterUrl/caddy/Caddyfile — $_" } # ── Step 4: secrets, .env, config.json ────────────────────────────── @@ -121,7 +123,8 @@ if ($writeConfig) { $envContent = @" HTTP_PORT=$PuterPort -# HTTPS_PORT=443 # uncomment after enabling TLS (see doc/selfhosting/full-stack.md) +# HTTPS_PORT=443 # uncomment after enabling TLS in caddy/Caddyfile +# # (see "Step 3 — TLS" in doc/self-hosting.md) MARIADB_ROOT_PASSWORD=$mariadbRootPw MARIADB_DATABASE=puter @@ -205,4 +208,4 @@ 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 'first-boot admin password is logged once — grab it with:' -Write-Log " cd $PuterDir; docker compose logs puter | Select-String password" \ No newline at end of file +Write-Log " cd $PuterDir; docker compose logs puter | Select-String password" diff --git a/install.sh b/install.sh index 2008c9eb5..2f524b1e2 100755 --- a/install.sh +++ b/install.sh @@ -7,7 +7,8 @@ # What this does, in order: # 1. Checks that docker (with the compose plugin), curl, and openssl exist. # 2. Creates ./puter-selfhosted/ (override with PUTER_DIR=...). -# 3. Downloads docker-compose.yml from the OSS repo (raw.githubusercontent.com). +# 3. Downloads docker-compose.yml + caddy/Caddyfile from the OSS repo +# (raw.githubusercontent.com). # 4. Generates fresh secrets and writes .env + puter/config/config.json. # 5. Runs `docker compose up -d` and prints the first-boot admin password. # @@ -19,15 +20,17 @@ # 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 nginx (default: 80) +# PUTER_PORT HTTP port for Caddy (default: 80) # PUTER_PROTOCOL public scheme: http | https (default: http) -# Set https when a TLS-terminating reverse proxy (Caddy, -# Traefik, nginx, a cloud LB) sits in front — Puter then -# builds https:// origins, S3 URLs, and redirects. See -# "Running behind your own reverse proxy" in doc/self-hosting.md. +# Set https once TLS is terminating in front of Puter — +# either the bundled Caddy with your certs (see "Step 3 — +# TLS" in doc/self-hosting.md) or your own proxy (Traefik, +# a cloud LB). Puter then builds https:// origins, S3 +# URLs, and redirects. Leave it http and the installer +# serves plain HTTP on PUTER_PORT. # PUTER_TRUST_PROXY number of reverse-proxy hops in front (default: 1) -# 1 = the bundled nginx (or a single external proxy); -# 2 = two hops (e.g. Cloudflare → your proxy → Puter). +# 1 = the bundled Caddy (or a single external proxy); +# 2 = two hops (e.g. Cloudflare → Caddy → Puter). # PUTER_ENV prod | dev (default: prod) # PUTER_FORCE set to 1 to overwrite existing .env / config.json @@ -72,25 +75,27 @@ mkdir -p puter/config puter/data puter/tls # bind-mount roots sidesteps the mismatch without guessing each image's # internal UID. (Docker Desktop on macOS/Windows papers over this with # its VM layer; native Linux docker on Debian/Alpine doesn't.) -mkdir -p puter/data/valkey puter/data/mariadb puter/data/dynamo puter/data/s3 puter/data/puter -chmod 0777 puter/data/valkey puter/data/mariadb puter/data/dynamo puter/data/s3 puter/data/puter +mkdir -p puter/data/valkey puter/data/mariadb puter/data/dynamo puter/data/s3 puter/data/puter puter/data/caddy +chmod 0777 puter/data/valkey puter/data/mariadb puter/data/dynamo puter/data/s3 puter/data/puter puter/data/caddy log "install dir: $(pwd)" -# ── Step 3: docker-compose.yml + nginx config ────────────────────── +# ── Step 3: docker-compose.yml + Caddy config ────────────────────── log "downloading docker-compose.yml from $PUTER_URL" curl -fsSL "$PUTER_URL/docker-compose.yml" -o docker-compose.yml \ || die "could not fetch $PUTER_URL/docker-compose.yml" -# nginx is mounted as `./nginx/nginx.conf:/etc/nginx/nginx.conf:ro` — if +# Caddy is mounted as `./caddy/Caddyfile:/etc/caddy/Caddyfile:ro,z` — if # the host file is missing, docker silently creates a directory at that # path and the mount fails with "not a directory" at container start. -log "downloading nginx/nginx.conf from $PUTER_URL" -mkdir -p nginx +# The Caddyfile is domain-agnostic — it answers on every Host and leaves +# the subdomain routing to Puter — so there's nothing to template in. +log "downloading caddy/Caddyfile from $PUTER_URL" +mkdir -p caddy # If the path was previously auto-created as a dir by a failed `compose up`, # remove it so curl can write the file. -[ -d nginx/nginx.conf ] && rmdir nginx/nginx.conf 2>/dev/null || true -curl -fsSL "$PUTER_URL/nginx/nginx.conf" -o nginx/nginx.conf \ - || die "could not fetch $PUTER_URL/nginx/nginx.conf" +[ -d caddy/Caddyfile ] && rmdir caddy/Caddyfile 2>/dev/null || true +curl -fsSL "$PUTER_URL/caddy/Caddyfile" -o caddy/Caddyfile \ + || die "could not fetch $PUTER_URL/caddy/Caddyfile" # ── Step 4: secrets, .env, config.json ────────────────────────────── write_config=1 @@ -113,7 +118,8 @@ if [ "$write_config" = "1" ]; then cat > .env <` subdomain so signature verification works - # (Host header preserved end-to-end) and so HTTPS stays clean - # (no mixed-content from a port-9000 host publish). - server s3:9000; - keepalive 32; - } - - # ── HTTP (port 80) ───────────────────────────────────────────── - server { - listen 80; - listen [::]:80; - server_name ~^s3\.; - - location / { - proxy_pass http://s3_backend; - proxy_http_version 1.1; - # Critical: preserve the original Host so RustFS validates - # the request against the same host the URL was signed for. - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - } - - server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - - # Note: when you enable TLS, replace this block with a redirect: - # return 301 https://$host$request_uri; - location / { - proxy_pass http://puter_backend; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } - } - - # ── HTTPS (port 443) — uncomment after dropping certs in ./puter/tls/ ─ - # server { - # listen 443 ssl; - # listen [::]:443 ssl; - # http2 on; - # server_name ~^s3\.; - # - # ssl_certificate /etc/nginx/tls/fullchain.pem; - # ssl_certificate_key /etc/nginx/tls/privkey.pem; - # ssl_protocols TLSv1.2 TLSv1.3; - # ssl_ciphers HIGH:!aNULL:!MD5; - # ssl_session_cache shared:SSL:10m; - # ssl_session_timeout 10m; - # - # location / { - # proxy_pass http://s3_backend; - # proxy_http_version 1.1; - # proxy_set_header Host $host; - # proxy_set_header X-Real-IP $remote_addr; - # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # proxy_set_header X-Forwarded-Proto $scheme; - # } - # } - # - # server { - # listen 443 ssl default_server; - # listen [::]:443 ssl default_server; - # http2 on; - # server_name _; - # - # ssl_certificate /etc/nginx/tls/fullchain.pem; - # ssl_certificate_key /etc/nginx/tls/privkey.pem; - # ssl_protocols TLSv1.2 TLSv1.3; - # ssl_ciphers HIGH:!aNULL:!MD5; - # ssl_session_cache shared:SSL:10m; - # ssl_session_timeout 10m; - # - # location / { - # proxy_pass http://puter_backend; - # proxy_http_version 1.1; - # proxy_set_header Host $host; - # proxy_set_header X-Real-IP $remote_addr; - # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # proxy_set_header X-Forwarded-Proto $scheme; - # proxy_set_header X-Forwarded-Host $host; - # proxy_set_header X-Forwarded-Port $server_port; - # proxy_set_header Upgrade $http_upgrade; - # proxy_set_header Connection $connection_upgrade; - # } - # } -}