diff --git a/README.md b/README.md index 9e2787b66..535eb5d7a 100644 --- a/README.md +++ b/README.md @@ -58,49 +58,15 @@ troubleshooting steps.
-### 🐳 Docker +### 🚀 Self-Hosting (one-line install) ```bash -mkdir puter && cd puter && mkdir -p puter/config puter/data && sudo chown -R 1000:1000 puter && docker run --rm -p 4100:4100 -v `pwd`/puter/config:/etc/puter -v `pwd`/puter/data:/var/puter ghcr.io/heyputer/puter +curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh ``` -**→** This should launch Puter at - http://puter.localhost:4100 (or the next available port). -
+Brings up Puter + every external service it needs (MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx) on a single host. Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker-compose.yml`, and runs `docker compose up -d`. -### 🐙 Docker Compose - -#### Linux/macOS - -```bash -mkdir -p puter/config puter/data -sudo chown -R 1000:1000 puter -wget https://raw.githubusercontent.com/HeyPuter/puter/main/docker-compose.yml -docker compose up -``` -**→** This should be available at - http://puter.localhost:4100 (or the next available port). - -
- -#### Windows - -```powershell -mkdir -p puter -cd puter -New-Item -Path "puter\config" -ItemType Directory -Force -New-Item -Path "puter\data" -ItemType Directory -Force -Invoke-WebRequest -Uri "https://raw.githubusercontent.com/HeyPuter/puter/main/docker-compose.yml" -OutFile "docker-compose.yml" -docker compose up -``` -**→** This should launch Puter at - http://puter.localhost:4100 (or the next available port). - -
- -### 🚀 Self-Hosting - -For detailed guides on self-hosting Puter, including configuration options and best practices, see our [Self-Hosting Documentation](https://github.com/HeyPuter/puter/blob/main/doc/self-hosters/instructions.md). +For the manual walkthrough, TLS setup, and post-install configuration knobs, see [Self-Hosting Documentation](./doc/selfhosting/README.md).
diff --git a/doc/selfhosting/README.md b/doc/selfhosting/README.md index 899e64b1d..4e12e1922 100644 --- a/doc/selfhosting/README.md +++ b/doc/selfhosting/README.md @@ -1,12 +1,11 @@ # Self-hosting Puter -Three supported ways to run Puter, in increasing order of effort and capability. Pick one, follow that page, ignore the others. +Two supported ways to run Puter. Pick one, follow that page. -| Mode | Best for | External services | -| ------------------------------------------ | ----------------------------------------------------- | ----------------------------------------------------------- | -| [**1. Dev (npm start)**](./npm.md) | Hacking on the source / trying it on your laptop | None — everything runs in-process | -| [**2. Docker (single container)**](./docker.md) | Production single-host; bring your own DB / S3 | None bundled — point at services you already run | -| [**3. Full self-hosted stack**](./full-stack.md) | Production with a self-managed stack | Bundled: MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx | +| Mode | Best for | External services | +| ------------------------------------------------ | ------------------------------------------------ | ---------------------------------------------------------- | +| [**1. Dev (npm start)**](./npm.md) | Hacking on the source / trying it on your laptop | None — everything runs in-process | +| [**2. Full self-hosted stack**](./full-stack.md) | Production, single host | Bundled: MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx | --- @@ -16,10 +15,14 @@ Clone, `npm install`, `npm start`. Backend, GUI, and `puter.js` run from the sou **Not safe to expose publicly** — uses dev secrets and an in-process key store. -## 2. Docker (single container) → [docker.md](./docker.md) +## 2. Full self-hosted stack → [full-stack.md](./full-stack.md) -One `docker run` against `ghcr.io/heyputer/puter:latest`. Out of the box uses the same in-process defaults as dev mode; drop a `config.json` into the mounted `/etc/puter/` to point at real services (MariaDB, S3, DynamoDB, Redis) one block at a time. Best when you already operate the dependencies you want Puter to use. +`docker compose up -d` brings up Puter **plus every external service it needs** (MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx) wired together. Closest to production you can run on a single host; supports your own domain and TLS. -## 3. Full self-hosted stack → [full-stack.md](./full-stack.md) +The fastest path is the one-shot installer — fetches the compose file, generates secrets, writes `.env` + `config.json`, and brings the stack up: -`docker compose -f docker-compose.full.yml up -d` brings up Puter **plus every external service it needs** (MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx) wired together. Closest to production you can run on a single host; supports your own domain and TLS. Best when you want a public Puter and don't already run the dependencies. +```bash +curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh +``` + +See [full-stack.md](./full-stack.md) for the manual walkthrough, TLS setup, and post-install configuration knobs. diff --git a/doc/selfhosting/docker.md b/doc/selfhosting/docker.md deleted file mode 100644 index 6407a517c..000000000 --- a/doc/selfhosting/docker.md +++ /dev/null @@ -1,182 +0,0 @@ -# 2. Docker (single container) - -One Puter container. You bring your own database, S3, etc. — or run with the bundled in-process defaults for a quick spin. The image is multi-arch (`linux/amd64`, `linux/arm64`). - -## Requirements - -- **Docker** (any recent version). - -## Bare minimum — defaults, single command - -```bash -mkdir -p puter/config puter/data - -docker run -d \ - --name puter \ - --restart unless-stopped \ - -p 4100:4100 \ - -v $(pwd)/puter/config:/etc/puter \ - -v $(pwd)/puter/data:/var/puter \ - ghcr.io/heyputer/puter:latest -``` - -Open . With nothing in `puter/config/`, the in-process defaults kick in (same SQLite + dynalite + fauxqs + redis-mock as dev mode). State lands in `puter/data/`. Login is `admin` — temp password is printed once in `docker logs puter`. - -That's enough to confirm the image works. Now configure for real. - -## Add a config - -The container reads **`/etc/puter/config.json`** and deep-merges it on top of the bundled defaults. You only put the keys you want to change. - -```bash -cat > puter/config/config.json <<'JSON' -{ - "domain": "puter.example.com", - "protocol": "https", - "pub_port": 443, - "env": "prod", - - "jwt_secret": "REPLACE-WITH-openssl-rand-hex-64", - "url_signature_secret": "REPLACE-WITH-A-DIFFERENT-openssl-rand-hex-64" -} -JSON - -docker restart puter -``` - -> 🔒 **Always replace `jwt_secret` and `url_signature_secret`.** The defaults are baked into the public image. Generate with `openssl rand -hex 64`. - -Watch the logs: - -```bash -docker logs -f puter -``` - -Look for `[config] override from /etc/puter/config.json` — that's the success signal. - -## Wire to external services - -Drop the relevant block(s) into `config.json`. Mix and match. Restart with `docker restart puter` after any change. - -### MySQL / MariaDB - -Puter applies its schema on first boot when you set `migrationPaths`: - -```json -{ - "database": { - "engine": "mysql", - "host": "db.internal", "port": 3306, - "user": "puter", "password": "...", "database": "puter", - "migrationPaths": ["/opt/puter/dist/src/backend/clients/database/migrations/mysql"] - } -} -``` - -Two files run in order: `mysql_mig_1.sql` (tables) and `mysql_mig_2.sql` (default apps — editor, viewer, pdf, camera, player, recorder, git, dev-center, puter-linux). Both are idempotent — safe to re-run. - -### S3 (real or S3-compatible) - -```json -{ - "s3": { - "s3Config": { - "endpoint": "https://s3.example.com", - "accessKeyId": "...", "secretAccessKey": "...", - "region": "us-east-1" - } - }, - "s3_bucket": "my-puter-bucket", - "s3_region": "us-east-1" -} -``` - -The bucket must exist already — Puter doesn't create it. - -For real AWS S3 the example above works as-is — virtual-hosted DNS form (`.s3.amazonaws.com`) is the AWS SDK default. For S3-compatible servers (RustFS, MinIO, fauxqs), add `"forcePathStyle": true` inside the `s3Config` block — virtual-hosted DNS doesn't resolve there. - -> ⚠️ **S3 uses camelCase keys** (`accessKeyId` / `secretAccessKey`). DynamoDB below uses snake_case. They're not the same. - -### DynamoDB (real AWS) - -Provision the table externally (e.g. Terraform): - -```json -{ - "dynamo": { - "aws": { "access_key": "...", "secret_key": "...", "region": "us-east-1" } - } -} -``` - -The KV table is named `store-kv-v1`. Schema: hash `namespace` (S), range `key` (S), LSI `lsi1-index` on `lsi1` (S), TTL on `ttl`. - -### Redis / Valkey cluster - -Puter speaks ioredis cluster protocol. Real Redis cluster: - -```json -{ "redis": { "startupNodes": [{ "host": "redis-0", "port": 6379 }] } } -``` - -For a self-hosted single Valkey/Redis container, run it in cluster mode (one node, all 16384 slots assigned to itself) and turn off TLS: - -```json -{ - "redis": { - "startupNodes": [{ "host": "valkey", "port": 6379 }], - "tls": false - } -} -``` - -TLS defaults to on (matches the prod ElastiCache shape) — set `false` for plain-TCP self-host. - -## What persists? - -Anything your config points at `/var/puter/...` lives on the host via the `puter/data` mount (SQLite path, fauxqs data dirs if you use them, etc.). If you've moved every dependency to external services, the data volume is mostly empty and optional. - -## Updating - -```bash -docker pull ghcr.io/heyputer/puter:latest -docker rm -f puter -# re-run the docker run command above -``` - -Your `config.json` and persistent data are untouched. - -## Building the image yourself - -```bash -docker build -t puter . - -# Multi-arch (requires buildx, on by default in modern Docker): -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - -t your-registry/puter:latest \ - --push . -``` - -A `docker-compose.yml` in this directory has a commented-out `build:` block — uncomment it (and flip `pull_policy` to `never`) to build from your local checkout instead of pulling. - -## Troubleshooting - -**`docker logs puter` shows the container restarting.** -Almost always JSON syntax in `config.json`. Validate: `jq . puter/config/config.json`. - -**The config file isn't picked up.** -Confirm it resolves to `/etc/puter/config.json` *inside* the container: -```bash -docker exec puter cat /etc/puter/config.json -``` -Empty / missing → the volume mount path is wrong. - -**Healthcheck reports unhealthy but the site works.** -The healthcheck hits `puter.localhost:4100/test` from inside the container. If you changed `domain` or `port`, the check still uses defaults. The site itself is fine. - -**`Error: DynamoDB aws config requires both access_key and secret_key`.** -You wrote `accessKeyId` / `secretAccessKey` (the AWS SDK form) under `dynamo.aws`. DynamoDB config uses snake_case. See above. - -**Architecture mismatch on Apple Silicon / ARM hosts.** -Use the published `:latest` tag — it's already multi-arch. If you built locally with `docker build` on an Intel Mac, the resulting image will be `linux/amd64` only. diff --git a/doc/selfhosting/full-stack.md b/doc/selfhosting/full-stack.md index edc3c9b30..e9bfeb46b 100644 --- a/doc/selfhosting/full-stack.md +++ b/doc/selfhosting/full-stack.md @@ -1,6 +1,14 @@ -# 3. Full self-hosted stack +# Full self-hosted stack -`docker-compose.full.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, nginx — wired together. Closest thing to a production deployment you can self-manage on a single host. + +## One-line installer (recommended) + +```bash +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. ## Requirements @@ -176,7 +184,7 @@ Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`. 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.full.yml](../docker-compose.full.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 `nginx` service. 4. In `.env`, uncomment `HTTPS_PORT=443`. 5. In `config.json`, switch: ```json @@ -190,13 +198,13 @@ Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`. ## Step 4 — Bring it up ```bash -docker compose -f docker-compose.full.yml up -d +docker compose up -d ``` First boot takes ~30s while MariaDB initialises and Puter applies the schema + default apps. Watch: ```bash -docker compose -f docker-compose.full.yml logs -f puter +docker compose logs -f puter ``` Healthy startup: @@ -211,14 +219,14 @@ Healthy startup: Then open **** (or `http://` if you skipped TLS). Login is `admin` — the temp password is printed once in the puter container logs on first boot: ```bash -docker compose -f docker-compose.full.yml logs puter | grep tmp_password +docker compose logs puter | grep tmp_password ``` Change it in Settings after first login. ## Additional configuration -All optional. Drop any of the blocks below into `puter/config/config.json` and `docker compose -f docker-compose.full.yml restart puter`. See [config.template.jsonc](../config.template.jsonc) for the full list. Per-key documentation lives in [src/backend/types.ts](../src/backend/types.ts). +All optional. Drop any of the blocks below into `puter/config/config.json` and `docker compose restart puter`. See [config.template.jsonc](../config.template.jsonc) for the full list. Per-key documentation lives in [src/backend/types.ts](../src/backend/types.ts). ### Email (SMTP) @@ -328,21 +336,21 @@ The `ollama` and `ollama-init` services live behind a compose profile so they do ``` 3. Bring up with the `ai` profile: ```bash - docker compose -f docker-compose.full.yml --profile ai up -d - docker compose -f docker-compose.full.yml logs -f ollama-init + docker compose --profile ai up -d + docker compose logs -f ollama-init ``` `ollama-init` exits 0 once the model is pulled. Subsequent boots find the model already on disk and the pull is a fast no-op. Without `--profile ai`, the `ollama` containers stay down and Puter (with `enabled: false`) doesn't try to reach them — the rest of the stack runs identically. -For GPU acceleration (NVIDIA), uncomment the `deploy:` block under the `ollama` service in [docker-compose.full.yml](../docker-compose.full.yml). Requires `nvidia-container-toolkit` on the host. +For GPU acceleration (NVIDIA), uncomment the `deploy:` block under the `ollama` service in [docker-compose.yml](../docker-compose.yml). Requires `nvidia-container-toolkit` on the host. ## Building from source instead of pulling -If you want to test local Dockerfile changes against the full stack, uncomment the `build:` block in [docker-compose.full.yml](../docker-compose.full.yml) under the `puter` service, change `pull_policy: always` → `pull_policy: never`, then: +If you want to test local Dockerfile changes against the full stack, uncomment the `build:` block in [docker-compose.yml](../docker-compose.yml) under the `puter` service, change `pull_policy: always` → `pull_policy: never`, then: ```bash -docker compose -f docker-compose.full.yml up -d --build +docker compose up -d --build ``` --- @@ -351,17 +359,17 @@ docker compose -f docker-compose.full.yml up -d --build ```bash # update -docker compose -f docker-compose.full.yml pull -docker compose -f docker-compose.full.yml up -d +docker compose pull +docker compose up -d # logs -docker compose -f docker-compose.full.yml logs -f puter +docker compose logs -f puter # stop, keep data -docker compose -f docker-compose.full.yml down +docker compose down # stop, NUKE all state (irreversible) -docker compose -f docker-compose.full.yml down +docker compose down rm -rf puter/data ``` @@ -370,10 +378,10 @@ Migrations re-apply idempotently across pulls. Volumes are preserved. ## Troubleshooting **Site loads but I get "Bad Gateway" / nginx errors.** -The puter container failed to come up. `docker compose -f docker-compose.full.yml 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".** -First-boot temp password is logged once. Find it: `docker compose -f docker-compose.full.yml logs puter | grep "tmp_password"`. After login, change it in Settings. +First-boot temp password is logged once. Find it: `docker compose logs puter | grep "tmp_password"`. After login, change it in Settings. **Healthcheck reports unhealthy but the site works.** The healthcheck hits `puter.localhost:4100/test` from inside the container. If you changed `domain` or `port`, the check still uses defaults. The site itself is fine. @@ -382,7 +390,7 @@ The healthcheck hits `puter.localhost:4100/test` from inside the container. If y DNS propagates slowly. `dig puter.example.com` and `dig api.puter.example.com` should both return your server IP. If not, give it 5–60 minutes. **`docker compose up` hangs at "waiting for service to be healthy".** -`docker compose -f docker-compose.full.yml ps` shows which container is unhealthy. MariaDB takes ~20–30s on a cold boot; everything else under 5s. If something stays unhealthy, `logs ` will tell you why. +`docker compose ps` shows which container is unhealthy. MariaDB takes ~20–30s on a cold boot; everything else under 5s. If something stays unhealthy, `logs ` will tell you why. **`Error: DynamoDB aws config requires both access_key and secret_key`.** You wrote `accessKeyId` / `secretAccessKey` under `dynamo.aws`. That config block uses snake_case (`access_key` / `secret_key`). Only the `s3.s3Config` block uses camelCase. diff --git a/docker-compose.full.yml b/docker-compose.full.yml deleted file mode 100644 index 9134aa98e..000000000 --- a/docker-compose.full.yml +++ /dev/null @@ -1,285 +0,0 @@ ---- -# Self-hosted Puter — full stack. -# -# Brings up Puter + every external service it needs: -# - nginx : 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) -# - s3 : RustFS — S3-compatible object storage -# - s3-init : one-shot init container that creates the bucket -# - puter : the application -# -# Quick start: -# 1. Copy .env.example to .env (or set the variables in your shell). -# 2. Drop a config.json into ./puter/config/ — see selfhosted/full-stack.md -# for the example that pairs with this compose. -# 3. docker compose -f docker-compose.full.yml up -d -# -# Production: -# - Always replace the default passwords / S3 keys / Puter secrets. -# - Front Puter with TLS-terminating reverse proxy (Caddy / nginx). -# - Move state-bearing volumes to a backed-up location. - -services: - valkey: - image: valkey/valkey:8-alpine - container_name: puter-valkey - restart: unless-stopped - # Run as a single-node cluster so Puter's ioredis Cluster client - # (the only mode it speaks) can connect. On first boot we assign all - # 16384 slots to ourselves; subsequent boots find them already in - # nodes.conf and skip. `cluster-require-full-coverage no` keeps reads - # working if we ever land partial slots. - command: - - sh - - -c - - | - valkey-server \ - --port 6379 \ - --cluster-enabled yes \ - --cluster-config-file /data/nodes.conf \ - --cluster-node-timeout 5000 \ - --cluster-require-full-coverage no \ - --cluster-announce-ip valkey \ - --cluster-announce-port 6379 \ - --cluster-announce-bus-port 16379 \ - --appendonly yes \ - --save "60 1" & - SERVER_PID=$$! - until valkey-cli -p 6379 PING > /dev/null 2>&1; do sleep 0.5; done - if ! valkey-cli -p 6379 CLUSTER NODES | grep -q '0-16383'; then - valkey-cli -p 6379 CLUSTER ADDSLOTSRANGE 0 16383 - fi - wait $$SERVER_PID - volumes: - - ./puter/data/valkey:/data - healthcheck: - test: - ["CMD-SHELL", "valkey-cli -p 6379 cluster info | grep -q cluster_state:ok"] - interval: 5s - timeout: 3s - retries: 20 - start_period: 10s - - mariadb: - image: mariadb:11 - container_name: puter-mariadb - restart: unless-stopped - environment: - MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root-change-me} - MARIADB_DATABASE: ${MARIADB_DATABASE:-puter} - MARIADB_USER: ${MARIADB_USER:-puter} - MARIADB_PASSWORD: ${MARIADB_PASSWORD:-puter-change-me} - volumes: - - ./puter/data/mariadb:/var/lib/mysql - healthcheck: - # `healthcheck.sh` ships with the mariadb image; --connect verifies - # the server is accepting auth, not just listening on the socket. - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - interval: 5s - timeout: 5s - retries: 20 - start_period: 30s - - dynamo: - # Puter creates the `store-kv-v1` table itself on startup - # (config.dynamo.bootstrapTables = true does the work). - image: amazon/dynamodb-local:latest - container_name: puter-dynamo - restart: unless-stopped - user: "1000:1000" - working_dir: /home/dynamodblocal - command: - - "-jar" - - "DynamoDBLocal.jar" - - "-sharedDb" - - "-dbPath" - - "/home/dynamodblocal/data" - volumes: - - ./puter/data/dynamo:/home/dynamodblocal/data - - s3: - # RustFS — S3-compatible object storage. Drop-in alternative: - # MinIO (image: minio/minio, command: ["server", "/data", "--console-address", ":9001"]). - image: rustfs/rustfs:latest - container_name: puter-s3 - restart: unless-stopped - environment: - RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-puter} - RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-puter-secret-change-me} - volumes: - - ./puter/data/s3:/data - # Internal-only — browsers reach RustFS via nginx (`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. - # ports: - # - "9000:9000" - healthcheck: - # RustFS exposes /health on the S3 port. Use wget (curl is not in - # the slim image). - test: - [ - "CMD-SHELL", - "wget -qO- --tries=1 --timeout=2 http://localhost:9000/health || exit 1", - ] - interval: 5s - timeout: 3s - retries: 20 - 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. - image: amazon/aws-cli:latest - container_name: puter-s3-init - depends_on: - s3: - condition: service_healthy - environment: - 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 - entrypoint: - - /bin/sh - - -c - - | - set -e - endpoint=http://s3:9000 - bucket=${S3_BUCKET:-puter-local} - if aws --endpoint-url "$$endpoint" s3api head-bucket --bucket "$$bucket" 2>/dev/null; then - echo "bucket $$bucket already exists" - else - echo "creating bucket $$bucket" - aws --endpoint-url "$$endpoint" s3 mb "s3://$$bucket" - fi - restart: "no" - - # ── Optional: local LLM ─────────────────────────────────────────── - # Behind the `ai` compose profile — only starts when explicitly opted - # into. Bring up with: - # docker compose -f docker-compose.full.yml --profile ai up -d - # When enabled, also set in your `puter/config/config.json`: - # "providers": { "ollama": { "apiBaseUrl": "http://ollama:11434" } } - # When NOT enabled, set: - # "providers": { "ollama": { "enabled": false } } - # otherwise Puter spams `ECONNREFUSED 127.0.0.1:11434` on startup. - ollama: - profiles: ["ai"] - # CPU-only out of the box; uncomment the GPU `deploy:` block below - # if you've got nvidia-docker for much faster inference. Disk + RAM - # scale with the model — `tinyllama` (1.1B, ~640 MB on disk, ~700 - # MB RAM) is the cheapest sane default. Swap via OLLAMA_DEFAULT_MODEL. - image: ollama/ollama:latest - container_name: puter-ollama - restart: unless-stopped - volumes: - - ./puter/data/ollama:/root/.ollama - # Uncomment to expose Ollama directly on the host (`localhost:11434`) - # for `ollama` CLI / OpenAI-API compatible tools. Internal-only by default. - # ports: - # - "11434:11434" - healthcheck: - test: - ["CMD-SHELL", "ollama list >/dev/null 2>&1 || exit 1"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 15s - # GPU passthrough (NVIDIA). Requires nvidia-container-toolkit on host. - # deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: all - # capabilities: [gpu] - - ollama-init: - profiles: ["ai"] - # One-shot — ensures the default model is present. `ollama pull` is - # idempotent: present-and-up-to-date → fast no-op; missing → downloads. - image: ollama/ollama:latest - container_name: puter-ollama-init - depends_on: - ollama: - condition: service_healthy - environment: - OLLAMA_HOST: http://ollama:11434 - OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-tinyllama} - entrypoint: - - /bin/sh - - -c - - | - set -e - echo "[ollama-init] ensuring $${OLLAMA_DEFAULT_MODEL}" - ollama pull "$${OLLAMA_DEFAULT_MODEL}" - echo "[ollama-init] done" - restart: "no" - - puter: - # image: ghcr.io/heyputer/puter:latest - pull_policy: always - # Uncomment to build from this directory instead of pulling the published - # image. Also flip pull_policy to `never` so compose doesn't overwrite - # your local build by re-pulling :latest. - build: - context: . - # buildx-only: cross-compile to both archs in a single push - platforms: - # - linux/amd64 - - linux/arm64 - container_name: puter - restart: unless-stopped - depends_on: - valkey: - condition: service_healthy - mariadb: - condition: service_healthy - dynamo: - condition: service_started - s3-init: - condition: service_completed_successfully - # Internal-only: nginx reaches it on the compose network. Uncomment - # to also expose port 4100 directly on the host (useful for debugging). - # ports: - # - "4100:4100" - expose: - - "4100" - environment: - PUID: 1000 - PGID: 1000 - volumes: - # Drop your config.json here — see selfhosted/full-stack.md. - - ./puter/config:/etc/puter - # Persistent runtime data (anything your config points at /var/puter). - - ./puter/data/puter:/var/puter - healthcheck: - test: wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1 - interval: 30s - timeout: 3s - retries: 3 - start_period: 30s - - nginx: - image: nginx:1.27-alpine - container_name: puter-nginx - restart: unless-stopped - depends_on: - puter: - condition: service_started - ports: - - "${HTTP_PORT:-80}:80" - # Uncomment when you enable TLS in nginx/nginx.conf: - # - "${HTTPS_PORT:-443}:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - # TLS certs (fullchain.pem + privkey.pem). Read-only inside. - - ./puter/tls:/etc/nginx/tls:ro - healthcheck: - test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://localhost/ || exit 1"] - interval: 10s - timeout: 3s - retries: 5 - start_period: 5s diff --git a/docker-compose.yml b/docker-compose.yml index e5e1cb7bc..402d6f8f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,34 +1,290 @@ --- +# Self-hosted Puter — full stack. +# +# Brings up Puter + every external service it needs: +# - nginx : 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) +# - s3 : RustFS — S3-compatible object storage +# - s3-init : one-shot init container that creates the bucket +# - puter : the application +# +# Quick start: +# 1. Copy .env.example to .env (or set the variables in your shell). +# 2. Drop a config.json into ./puter/config/ — see selfhosted/full-stack.md +# for the example that pairs with this compose. +# 3. docker compose up -d +# +# Easiest path: +# curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh +# grabs this file, generates secrets, writes .env + config.json, and runs +# the compose up for you. +# +# Production: +# - Always replace the default passwords / S3 keys / Puter secrets. +# - Front Puter with TLS-terminating reverse proxy (Caddy / nginx). +# - Move state-bearing volumes to a backed-up location. + services: + valkey: + image: valkey/valkey:8-alpine + container_name: puter-valkey + restart: unless-stopped + # Run as a single-node cluster so Puter's ioredis Cluster client + # (the only mode it speaks) can connect. On first boot we assign all + # 16384 slots to ourselves; subsequent boots find them already in + # nodes.conf and skip. `cluster-require-full-coverage no` keeps reads + # working if we ever land partial slots. + command: + - sh + - -c + - | + valkey-server \ + --port 6379 \ + --cluster-enabled yes \ + --cluster-config-file /data/nodes.conf \ + --cluster-node-timeout 5000 \ + --cluster-require-full-coverage no \ + --cluster-announce-ip valkey \ + --cluster-announce-port 6379 \ + --cluster-announce-bus-port 16379 \ + --appendonly yes \ + --save "60 1" & + SERVER_PID=$$! + until valkey-cli -p 6379 PING > /dev/null 2>&1; do sleep 0.5; done + if ! valkey-cli -p 6379 CLUSTER NODES | grep -q '0-16383'; then + valkey-cli -p 6379 CLUSTER ADDSLOTSRANGE 0 16383 + fi + wait $$SERVER_PID + volumes: + - ./puter/data/valkey:/data + healthcheck: + test: + ["CMD-SHELL", "valkey-cli -p 6379 cluster info | grep -q cluster_state:ok"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 10s + + mariadb: + image: mariadb:11 + container_name: puter-mariadb + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root-change-me} + MARIADB_DATABASE: ${MARIADB_DATABASE:-puter} + MARIADB_USER: ${MARIADB_USER:-puter} + MARIADB_PASSWORD: ${MARIADB_PASSWORD:-puter-change-me} + volumes: + - ./puter/data/mariadb:/var/lib/mysql + healthcheck: + # `healthcheck.sh` ships with the mariadb image; --connect verifies + # the server is accepting auth, not just listening on the socket. + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 30s + + dynamo: + # Puter creates the `store-kv-v1` table itself on startup + # (config.dynamo.bootstrapTables = true does the work). + image: amazon/dynamodb-local:latest + container_name: puter-dynamo + restart: unless-stopped + user: "1000:1000" + working_dir: /home/dynamodblocal + command: + - "-jar" + - "DynamoDBLocal.jar" + - "-sharedDb" + - "-dbPath" + - "/home/dynamodblocal/data" + volumes: + - ./puter/data/dynamo:/home/dynamodblocal/data + + s3: + # RustFS — S3-compatible object storage. Drop-in alternative: + # MinIO (image: minio/minio, command: ["server", "/data", "--console-address", ":9001"]). + image: rustfs/rustfs:latest + container_name: puter-s3 + restart: unless-stopped + environment: + RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-puter} + RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-puter-secret-change-me} + volumes: + - ./puter/data/s3:/data + # Internal-only — browsers reach RustFS via nginx (`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. + # ports: + # - "9000:9000" + healthcheck: + # RustFS exposes /health on the S3 port. Use wget (curl is not in + # the slim image). + test: + [ + "CMD-SHELL", + "wget -qO- --tries=1 --timeout=2 http://localhost:9000/health || exit 1", + ] + interval: 5s + timeout: 3s + retries: 20 + 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. + image: amazon/aws-cli:latest + container_name: puter-s3-init + depends_on: + s3: + condition: service_healthy + environment: + 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 + entrypoint: + - /bin/sh + - -c + - | + set -e + endpoint=http://s3:9000 + bucket=${S3_BUCKET:-puter-local} + if aws --endpoint-url "$$endpoint" s3api head-bucket --bucket "$$bucket" 2>/dev/null; then + echo "bucket $$bucket already exists" + else + echo "creating bucket $$bucket" + aws --endpoint-url "$$endpoint" s3 mb "s3://$$bucket" + fi + restart: "no" + + # ── Optional: local LLM ─────────────────────────────────────────── + # Behind the `ai` compose profile — only starts when explicitly opted + # into. Bring up with: + # docker compose --profile ai up -d + # When enabled, also set in your `puter/config/config.json`: + # "providers": { "ollama": { "apiBaseUrl": "http://ollama:11434" } } + # When NOT enabled, set: + # "providers": { "ollama": { "enabled": false } } + # otherwise Puter spams `ECONNREFUSED 127.0.0.1:11434` on startup. + ollama: + profiles: ["ai"] + # CPU-only out of the box; uncomment the GPU `deploy:` block below + # if you've got nvidia-docker for much faster inference. Disk + RAM + # scale with the model — `tinyllama` (1.1B, ~640 MB on disk, ~700 + # MB RAM) is the cheapest sane default. Swap via OLLAMA_DEFAULT_MODEL. + image: ollama/ollama:latest + container_name: puter-ollama + restart: unless-stopped + volumes: + - ./puter/data/ollama:/root/.ollama + # Uncomment to expose Ollama directly on the host (`localhost:11434`) + # for `ollama` CLI / OpenAI-API compatible tools. Internal-only by default. + # ports: + # - "11434:11434" + healthcheck: + test: + ["CMD-SHELL", "ollama list >/dev/null 2>&1 || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + # GPU passthrough (NVIDIA). Requires nvidia-container-toolkit on host. + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: all + # capabilities: [gpu] + + ollama-init: + profiles: ["ai"] + # One-shot — ensures the default model is present. `ollama pull` is + # idempotent: present-and-up-to-date → fast no-op; missing → downloads. + image: ollama/ollama:latest + container_name: puter-ollama-init + depends_on: + ollama: + condition: service_healthy + environment: + OLLAMA_HOST: http://ollama:11434 + OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-tinyllama} + entrypoint: + - /bin/sh + - -c + - | + set -e + echo "[ollama-init] ensuring $${OLLAMA_DEFAULT_MODEL}" + ollama pull "$${OLLAMA_DEFAULT_MODEL}" + echo "[ollama-init] done" + restart: "no" + puter: - container_name: puter - image: ghcr.io/heyputer/puter:latest + image: ghcr.io/heyputer/puter:main pull_policy: always - # Uncomment to build from this directory instead of pulling the published image: + # Uncomment to build from this directory instead of pulling the published + # image. Also flip pull_policy to `never` so compose doesn't overwrite + # your local build by re-pulling :latest. # build: # context: . # # buildx-only: cross-compile to both archs in a single push # # platforms: # # - linux/amd64 # # - linux/arm64 + container_name: puter restart: unless-stopped - ports: - - '4100:4100' + depends_on: + valkey: + condition: service_healthy + mariadb: + condition: service_healthy + dynamo: + condition: service_started + s3-init: + condition: service_completed_successfully + # Internal-only: nginx reaches it on the compose network. Uncomment + # to also expose port 4100 directly on the host (useful for debugging). + # ports: + # - "4100:4100" + expose: + - "4100" environment: - # TZ: Europe/Paris PUID: 1000 PGID: 1000 volumes: - # Drop your config.json into ./puter/config/. It is deep-merged over - # config.default.json — only override what you care to change. - # Image expects /etc/puter/config.json (see PUTER_CONFIG_PATH in Dockerfile). + # Drop your config.json here — see selfhosted/full-stack.md. - ./puter/config:/etc/puter - # Persistent runtime data (sqlite db, uploads, etc. — depends on your - # config). Maps to volatile/ inside the container by default. - - ./puter/data:/var/puter + # Persistent runtime data (anything your config points at /var/puter). + - ./puter/data/puter:/var/puter healthcheck: test: wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1 interval: 30s timeout: 3s retries: 3 start_period: 30s + + nginx: + image: nginx:1.27-alpine + container_name: puter-nginx + restart: unless-stopped + depends_on: + puter: + condition: service_started + ports: + - "${HTTP_PORT:-80}:80" + # Uncomment when you enable TLS in nginx/nginx.conf: + # - "${HTTPS_PORT:-443}:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + # TLS certs (fullchain.pem + privkey.pem). Read-only inside. + - ./puter/tls:/etc/nginx/tls:ro + healthcheck: + test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://localhost/ || exit 1"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s diff --git a/install.sh b/install.sh new file mode 100755 index 000000000..7d514e2bb --- /dev/null +++ b/install.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env sh +# Self-hosted Puter — one-shot installer. +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh +# +# 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). +# 4. Generates fresh secrets and writes .env + puter/config/config.json. +# 5. Runs `docker compose up -d` and prints the first-boot admin password. +# +# Re-running the script in an already-initialised directory is a no-op for +# config (it won't clobber existing .env / config.json) and just refreshes +# the compose file + brings the stack up. Set PUTER_FORCE=1 to overwrite. +# +# Tunable env vars: +# 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_FORCE set to 1 to overwrite existing .env / config.json + +set -eu + +PUTER_DIR="${PUTER_DIR:-puter-selfhosted}" +PUTER_URL="${PUTER_URL:-https://raw.githubusercontent.com/HeyPuter/puter/main}" +PUTER_DOMAIN="${PUTER_DOMAIN:-puter.localhost}" +PUTER_PORT="${PUTER_PORT:-80}" +PUTER_FORCE="${PUTER_FORCE:-0}" + +log() { printf '\033[1;36m[puter-install]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[puter-install]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31m[puter-install]\033[0m %s\n' "$*" >&2; exit 1; } + +need() { + command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" +} + +# ── Step 1: dependency check ──────────────────────────────────────── +log "checking dependencies" +need docker +need curl +need openssl +docker compose version >/dev/null 2>&1 \ + || die "docker compose plugin not found — install docker desktop or 'docker-compose-plugin'" + +# ── Step 2: install dir ───────────────────────────────────────────── +mkdir -p "$PUTER_DIR" +cd "$PUTER_DIR" +mkdir -p puter/config puter/data puter/tls +log "install dir: $(pwd)" + +# ── Step 3: docker-compose.yml ────────────────────────────────────── +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" + +# ── Step 4: secrets, .env, config.json ────────────────────────────── +write_config=1 +if [ -f .env ] && [ -f puter/config/config.json ] && [ "$PUTER_FORCE" != "1" ]; then + log ".env + config.json already present — keeping existing secrets (PUTER_FORCE=1 to overwrite)" + write_config=0 +fi + +if [ "$write_config" = "1" ]; then + log "generating secrets" + MARIADB_ROOT_PASSWORD=$(openssl rand -hex 32) + MARIADB_PASSWORD=$(openssl rand -hex 32) + S3_SECRET_KEY=$(openssl rand -hex 32) + JWT_SECRET=$(openssl rand -hex 64) + URL_SIGNATURE_SECRET=$(openssl rand -hex 64) + + cat > .env < puter/config/config.json <