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 <daniel.salazar@puter.com>
This commit is contained in:
Sauhbagya Prasad
2026-07-28 14:52:13 -07:00
committed by GitHub
co-authored by Daniel Salazar
parent 0a61b4e78b
commit 7cf3e03433
7 changed files with 168 additions and 195 deletions
+2 -2
View File
@@ -2,9 +2,9 @@
# docker-compose.full.yml up -d`. None of the defaults below are safe for # docker-compose.full.yml up -d`. None of the defaults below are safe for
# anything beyond a local laptop test. # anything beyond a local laptop test.
# ── Public-facing ports (nginx) --------------------------------------- # ── Public-facing ports (Caddy) ---------------------------------------
HTTP_PORT=80 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 ------------------------------------------------------------
MARIADB_ROOT_PASSWORD=replace-with-strong-password MARIADB_ROOT_PASSWORD=replace-with-strong-password
+73
View File
@@ -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 (<name>.site.<domain>,
# <name>.app.<domain>) — 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
# }
+20 -16
View File
@@ -1,6 +1,6 @@
# Self-Hosting Puter # 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) ## One-line installer (recommended)
@@ -8,7 +8,7 @@
curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh 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 ## Requirements
@@ -20,7 +20,7 @@ Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker
| Container | Image | Role | | 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` | `ghcr.io/heyputer/puter` | The app |
| `puter-mariadb` | `mariadb:11` | SQL database — schema applied automatically on first boot | | `puter-mariadb` | `mariadb:11` | SQL database — schema applied automatically on first boot |
| `puter-valkey` | `valkey/valkey:8-alpine` | Redis-compatible cache + rate-limiter | | `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 S3_BUCKET=puter-local
EOF 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 <<EOF cat > puter/config/config.json <<EOF
{ {
"domain": "puter.localhost", "domain": "puter.localhost",
@@ -141,14 +141,14 @@ Why these knobs:
- `dynamo.aws` keys are dummies; DynamoDB-local doesn't validate them but the AWS SDK requires _something_. **Note:** DynamoDB uses `access_key` / `secret_key` (snake_case); S3 below uses `accessKeyId` / `secretAccessKey` (camelCase). Not interchangeable. - `dynamo.aws` keys are dummies; DynamoDB-local doesn't validate them but the AWS SDK requires _something_. **Note:** DynamoDB uses `access_key` / `secret_key` (snake_case); S3 below uses `accessKeyId` / `secretAccessKey` (camelCase). Not interchangeable.
- `providers.ollama.enabled: false` — Puter auto-probes a local Ollama at `127.0.0.1:11434` by default; without one running you'd see `ECONNREFUSED` on every boot. To run a bundled Ollama, see [Optional: local LLM (Ollama)](#optional-local-llm-ollama) below. - `providers.ollama.enabled: false` — Puter auto-probes a local Ollama at `127.0.0.1:11434` by default; without one running you'd see `ECONNREFUSED` on every boot. To run a bundled Ollama, see [Optional: local LLM (Ollama)](#optional-local-llm-ollama) below.
- `s3.s3Config.forcePathStyle: true` — RustFS / MinIO / fauxqs need path-style URLs (`<endpoint>/<bucket>`). Real AWS S3 wants virtual-hosted (`<bucket>.<endpoint>`) — drop this flag (or set `false`) when you swap to real S3. - `s3.s3Config.forcePathStyle: true` — RustFS / MinIO / fauxqs need path-style URLs (`<endpoint>/<bucket>`). Real AWS S3 wants virtual-hosted (`<bucket>.<endpoint>`) — 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.<domain>` 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.<your-domain>` once you enable TLS in Step 3. Real AWS S3 doesn't need this — its endpoint is already public; drop the field entirely. - `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.<domain>` 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.<your-domain>` 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). - `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. > 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\] ## 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 → <your server's public IP> A puter.localhost → <your server's public IP>
@@ -163,7 +163,7 @@ A dev.puter.localhost → <your server's public IP>
A *.dev.puter.localhost → <your server's public IP> A *.dev.puter.localhost → <your server's public IP>
``` ```
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\] ## 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. 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 (`<name>.site.<domain>`, `<name>.app.<domain>`), 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/<provider>`) and swap the `tls` line for a `tls { dns <provider> … }` block.
Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`. 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). 1. Open [caddy/Caddyfile](../caddy/Caddyfile) and uncomment the `# :443 { … }` block at the bottom.
2. (Optional) Replace the body of the port-80 blocks with `return 301 https://$host$request_uri;` to force HTTPS everywhere. 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 `nginx` service. 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`.
5. In `config.json`, switch: 5. In `config.json`, switch:
```json ```json
@@ -201,9 +203,9 @@ Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`.
## Running behind your own reverse proxy ## 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): **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). - `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. - `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 `*.<domain>` (covering `api.*`, `app.*`, and `s3.<domain>`) and `*.site.<domain>` (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. 6. **Route the wildcard to Puter.** Your proxy must forward `*.<domain>` (covering `api.*`, `app.*`, and `s3.<domain>`) and `*.site.<domain>` (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 ## 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 ```bash
docker compose up -d docker compose up -d
``` ```
@@ -469,7 +473,7 @@ Migrations re-apply idempotently across pulls. Volumes are preserved.
## Troubleshooting ## 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`). 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".** **Login screen says "admin password not set".**
+22 -12
View File
@@ -2,7 +2,7 @@
# Self-hosted Puter — full stack. # Self-hosted Puter — full stack.
# #
# Brings up Puter + every external service it needs: # 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 # - valkey : redis-compatible cache / rate-limiter backend
# - mariadb : SQL database (Puter applies its schema on first boot) # - mariadb : SQL database (Puter applies its schema on first boot)
# - dynamo : DynamoDB-local (KV store; Puter creates the table itself) # - dynamo : DynamoDB-local (KV store; Puter creates the table itself)
@@ -23,7 +23,8 @@
# #
# Production: # Production:
# - Always replace the default passwords / S3 keys / Puter secrets. # - 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. # - Move state-bearing volumes to a backed-up location.
services: services:
@@ -118,7 +119,7 @@ services:
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-puter-secret-change-me} RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-puter-secret-change-me}
volumes: volumes:
- ./puter/data/s3:/data:z - ./puter/data/s3:/data:z
# Internal-only — browsers reach RustFS via nginx (`s3.<domain>`), # Internal-only — browsers reach RustFS via Caddy (`s3.<domain>`),
# which preserves the Host header for S3 signature validation and # which preserves the Host header for S3 signature validation and
# rides the same TLS termination as Puter. Uncomment to also expose # rides the same TLS termination as Puter. Uncomment to also expose
# 9000 directly on the host for `aws-cli` / debugging. # 9000 directly on the host for `aws-cli` / debugging.
@@ -249,7 +250,7 @@ services:
condition: service_started condition: service_started
s3-init: s3-init:
condition: service_completed_successfully 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). # to also expose port 4100 directly on the host (useful for debugging).
# ports: # ports:
# - "4100:4100" # - "4100:4100"
@@ -270,23 +271,32 @@ services:
retries: 3 retries: 3
start_period: 30s start_period: 30s
nginx: caddy:
image: nginx:1.27-alpine image: caddy:2.11-alpine
container_name: puter-nginx container_name: puter-caddy
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
puter: puter:
condition: service_started condition: service_started
ports: ports:
- "${HTTP_PORT:-80}:80" - "${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" # - "${HTTPS_PORT:-443}:443"
volumes: volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro,z - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro,z
# TLS certs (fullchain.pem + privkey.pem). Read-only inside. # TLS certs (wildcard fullchain.pem + privkey.pem). Read-only inside.
- ./puter/tls:/etc/nginx/tls:ro,z - ./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: 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 interval: 10s
timeout: 3s timeout: 3s
retries: 5 retries: 5
+15 -12
View File
@@ -11,7 +11,7 @@
# What this does, in order: # What this does, in order:
# 1. Checks that docker (with the compose plugin) exists. # 1. Checks that docker (with the compose plugin) exists.
# 2. Creates ./puter-selfhosted/ (override with $env:PUTER_DIR). # 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. # 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. # 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_DIR install directory (default: ./puter-selfhosted)
# PUTER_URL base URL to fetch docker-compose.yml (default: GitHub raw, main branch) # 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_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 # PUTER_FORCE set to 1 to overwrite existing .env / config.json
[CmdletBinding()] [CmdletBinding()]
@@ -77,10 +77,10 @@ if ($LASTEXITCODE -ne 0) {
# ── Step 2: install dir ───────────────────────────────────────────── # ── Step 2: install dir ─────────────────────────────────────────────
$null = New-Item -ItemType Directory -Force -Path $PuterDir $null = New-Item -ItemType Directory -Force -Path $PuterDir
Set-Location $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)" 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" Write-Log "downloading docker-compose.yml from $PuterUrl"
try { try {
Invoke-WebRequest -Uri "$PuterUrl/docker-compose.yml" -OutFile 'docker-compose.yml' -UseBasicParsing 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 — $_" Die "could not fetch $PuterUrl/docker-compose.yml — $_"
} }
Write-Log "downloading nginx/nginx.conf from $PuterUrl" # The Caddyfile is domain-agnostic — it answers on every Host and leaves
$null = New-Item -ItemType Directory -Force -Path 'nginx' # 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 # If the path was previously auto-created as a directory by a failed
# `compose up`, remove it so we can write the file there. # `compose up`, remove it so we can write the file there.
if (Test-Path 'nginx/nginx.conf' -PathType Container) { if (Test-Path 'caddy/Caddyfile' -PathType Container) {
Remove-Item 'nginx/nginx.conf' -Recurse -Force Remove-Item 'caddy/Caddyfile' -Recurse -Force
} }
try { try {
Invoke-WebRequest -Uri "$PuterUrl/nginx/nginx.conf" -OutFile 'nginx/nginx.conf' -UseBasicParsing Invoke-WebRequest -Uri "$PuterUrl/caddy/Caddyfile" -OutFile 'caddy/Caddyfile' -UseBasicParsing
} catch { } catch {
Die "could not fetch $PuterUrl/nginx/nginx.conf$_" Die "could not fetch $PuterUrl/caddy/Caddyfile$_"
} }
# ── Step 4: secrets, .env, config.json ────────────────────────────── # ── Step 4: secrets, .env, config.json ──────────────────────────────
@@ -121,7 +123,8 @@ if ($writeConfig) {
$envContent = @" $envContent = @"
HTTP_PORT=$PuterPort 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_ROOT_PASSWORD=$mariadbRootPw
MARIADB_DATABASE=puter MARIADB_DATABASE=puter
@@ -205,4 +208,4 @@ Write-Log " cd $PuterDir; docker compose logs -f puter"
Write-Log '' Write-Log ''
Write-Log "open http://${PuterDomain}:${PuterPort} once the puter container is healthy." 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 'first-boot admin password is logged once — grab it with:'
Write-Log " cd $PuterDir; docker compose logs puter | Select-String password" Write-Log " cd $PuterDir; docker compose logs puter | Select-String password"
+36 -18
View File
@@ -7,7 +7,8 @@
# What this does, in order: # What this does, in order:
# 1. Checks that docker (with the compose plugin), curl, and openssl exist. # 1. Checks that docker (with the compose plugin), curl, and openssl exist.
# 2. Creates ./puter-selfhosted/ (override with PUTER_DIR=...). # 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. # 4. Generates fresh secrets and writes .env + puter/config/config.json.
# 5. Runs `docker compose up -d` and prints the first-boot admin password. # 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_DIR install directory (default: ./puter-selfhosted)
# PUTER_URL base URL to fetch docker-compose.yml (default: GitHub raw, main branch) # 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_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) # PUTER_PROTOCOL public scheme: http | https (default: http)
# Set https when a TLS-terminating reverse proxy (Caddy, # Set https once TLS is terminating in front of Puter —
# Traefik, nginx, a cloud LB) sits in front — Puter then # either the bundled Caddy with your certs (see "Step 3 —
# builds https:// origins, S3 URLs, and redirects. See # TLS" in doc/self-hosting.md) or your own proxy (Traefik,
# "Running behind your own reverse proxy" in doc/self-hosting.md. # 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) # PUTER_TRUST_PROXY number of reverse-proxy hops in front (default: 1)
# 1 = the bundled nginx (or a single external proxy); # 1 = the bundled Caddy (or a single external proxy);
# 2 = two hops (e.g. Cloudflare → your proxy → Puter). # 2 = two hops (e.g. Cloudflare → Caddy → Puter).
# PUTER_ENV prod | dev (default: prod) # PUTER_ENV prod | dev (default: prod)
# PUTER_FORCE set to 1 to overwrite existing .env / config.json # 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 # bind-mount roots sidesteps the mismatch without guessing each image's
# internal UID. (Docker Desktop on macOS/Windows papers over this with # internal UID. (Docker Desktop on macOS/Windows papers over this with
# its VM layer; native Linux docker on Debian/Alpine doesn't.) # 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 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 chmod 0777 puter/data/valkey puter/data/mariadb puter/data/dynamo puter/data/s3 puter/data/puter puter/data/caddy
log "install dir: $(pwd)" 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" log "downloading docker-compose.yml from $PUTER_URL"
curl -fsSL "$PUTER_URL/docker-compose.yml" -o docker-compose.yml \ curl -fsSL "$PUTER_URL/docker-compose.yml" -o docker-compose.yml \
|| die "could not fetch $PUTER_URL/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 # the host file is missing, docker silently creates a directory at that
# path and the mount fails with "not a directory" at container start. # path and the mount fails with "not a directory" at container start.
log "downloading nginx/nginx.conf from $PUTER_URL" # The Caddyfile is domain-agnostic — it answers on every Host and leaves
mkdir -p nginx # 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`, # If the path was previously auto-created as a dir by a failed `compose up`,
# remove it so curl can write the file. # remove it so curl can write the file.
[ -d nginx/nginx.conf ] && rmdir nginx/nginx.conf 2>/dev/null || true [ -d caddy/Caddyfile ] && rmdir caddy/Caddyfile 2>/dev/null || true
curl -fsSL "$PUTER_URL/nginx/nginx.conf" -o nginx/nginx.conf \ curl -fsSL "$PUTER_URL/caddy/Caddyfile" -o caddy/Caddyfile \
|| die "could not fetch $PUTER_URL/nginx/nginx.conf" || die "could not fetch $PUTER_URL/caddy/Caddyfile"
# ── Step 4: secrets, .env, config.json ────────────────────────────── # ── Step 4: secrets, .env, config.json ──────────────────────────────
write_config=1 write_config=1
@@ -113,7 +118,8 @@ if [ "$write_config" = "1" ]; then
cat > .env <<EOF cat > .env <<EOF
HTTP_PORT=$PUTER_PORT HTTP_PORT=$PUTER_PORT
# 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=$MARIADB_ROOT_PASSWORD MARIADB_ROOT_PASSWORD=$MARIADB_ROOT_PASSWORD
MARIADB_DATABASE=puter MARIADB_DATABASE=puter
@@ -190,6 +196,18 @@ EOF
EOF EOF
fi fi
# `https` only works if something in front is actually terminating it. The
# bundled Caddy does that from ./puter/tls once you enable its `:443` block
# (see "Step 3 — TLS" in doc/self-hosting.md); your own edge proxy is fine
# too. Neither in place means Puter hands out https:// URLs for an endpoint
# that only speaks HTTP — broken logins and mixed content.
if [ "$PUTER_PROTOCOL" = "https" ] && [ ! -f puter/tls/fullchain.pem ]; then
warn "PUTER_PROTOCOL=https but ./puter/tls/fullchain.pem is missing."
warn "The bundled Caddy is still serving plain HTTP on port $PUTER_PORT."
warn "Either drop a wildcard cert into ./puter/tls and uncomment the :443"
warn "block in caddy/Caddyfile, or terminate TLS in your own proxy."
fi
# ── Step 5: bring it up ───────────────────────────────────────────── # ── Step 5: bring it up ─────────────────────────────────────────────
log "docker compose up -d" log "docker compose up -d"
docker compose up -d docker compose up -d
-135
View File
@@ -1,135 +0,0 @@
# 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.*, etc).
#
# To enable TLS:
# 1. Drop your fullchain.pem + privkey.pem into ./puter/tls/.
# 2. Uncomment the 443 server{} block below.
# 3. Update server_name to your domain (and wildcard subdomains).
worker_processes auto;
events {
worker_connections 4096;
}
http {
# Required for Puter's WebSocket / socket.io upgrades.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Rough size cap that mirrors prod ALB defaults; tune for your
# uploads. Puter chunks large uploads, so 1 GiB per request is plenty.
client_max_body_size 1024m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_buffering off;
server_tokens off;
upstream puter_backend {
server puter:4100;
keepalive 32;
}
upstream s3_backend {
# RustFS — see `s3` service in docker-compose.full.yml. Browsers
# PUT/GET here for presigned-URL uploads / downloads. Routed via
# the `s3.<domain>` 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;
# }
# }
}