docker-compose from index (#2909)

This commit is contained in:
Daniel Salazar
2026-05-05 00:22:25 -07:00
committed by GitHub
parent a5cb484e52
commit 838d6a7178
7 changed files with 476 additions and 547 deletions
+4 -38
View File
@@ -58,49 +58,15 @@ troubleshooting steps.
<br/>
### 🐳 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
<font color="red"> http://puter.localhost:4100 (or the next available port). </font>
<br/>
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
<font color="red"> http://puter.localhost:4100 (or the next available port). </font>
<br/>
#### 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
<font color="red"> http://puter.localhost:4100 (or the next available port). </font>
<br/>
### 🚀 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).
<br/>
+13 -10
View File
@@ -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.
-182
View File
@@ -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 <http://puter.localhost:4100>. 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 (`<bucket>.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.
+28 -20
View File
@@ -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 **<https://puter.local>** (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 560 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 ~2030s on a cold boot; everything else under 5s. If something stays unhealthy, `logs <service>` will tell you why.
`docker compose ps` shows which container is unhealthy. MariaDB takes ~2030s on a cold boot; everything else under 5s. If something stays unhealthy, `logs <service>` 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.
-285
View File
@@ -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.<domain>`),
# 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
+268 -12
View File
@@ -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.<domain>`),
# 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
Executable
+163
View File
@@ -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 <<EOF
HTTP_PORT=$PUTER_PORT
# HTTPS_PORT=443 # uncomment after enabling TLS (see doc/selfhosting/full-stack.md)
MARIADB_ROOT_PASSWORD=$MARIADB_ROOT_PASSWORD
MARIADB_DATABASE=puter
MARIADB_USER=puter
MARIADB_PASSWORD=$MARIADB_PASSWORD
S3_ACCESS_KEY=puter
S3_SECRET_KEY=$S3_SECRET_KEY
S3_BUCKET=puter-local
EOF
log "writing puter/config/config.json"
cat > puter/config/config.json <<EOF
{
"domain": "$PUTER_DOMAIN",
"protocol": "http",
"pub_port": $PUTER_PORT,
"env": "prod",
"static_hosting_domain": "site.$PUTER_DOMAIN",
"static_hosting_domain_alt": "host.$PUTER_DOMAIN",
"private_app_hosting_domain": "app.$PUTER_DOMAIN",
"private_app_hosting_domain_alt": "dev.$PUTER_DOMAIN",
"jwt_secret": "$JWT_SECRET",
"url_signature_secret": "$URL_SIGNATURE_SECRET",
"database": {
"engine": "mysql",
"host": "mariadb",
"port": 3306,
"user": "puter",
"password": "$MARIADB_PASSWORD",
"database": "puter",
"migrationPaths": ["/opt/puter/dist/src/backend/clients/database/migrations/mysql"]
},
"redis": {
"startupNodes": [{ "host": "valkey", "port": 6379 }],
"tls": false
},
"dynamo": {
"endpoint": "http://dynamo:8000",
"bootstrapTables": true,
"aws": {
"access_key": "fake",
"secret_key": "fake",
"region": "us-east-1"
}
},
"s3": {
"s3Config": {
"endpoint": "http://s3:9000",
"publicEndpoint": "http://s3.$PUTER_DOMAIN",
"accessKeyId": "puter",
"secretAccessKey": "$S3_SECRET_KEY",
"region": "us-east-1",
"forcePathStyle": true
}
},
"s3_bucket": "puter-local",
"s3_region": "us-east-1",
"providers": {
"ollama": { "enabled": false }
},
"trust_proxy": 1
}
EOF
fi
# ── Step 5: bring it up ─────────────────────────────────────────────
log "docker compose up -d"
docker compose up -d
log ""
log "stack starting. first boot takes ~30s while MariaDB initialises."
log "follow puter logs:"
log " cd $PUTER_DIR && docker compose logs -f puter"
log ""
log "open http://$PUTER_DOMAIN:$PUTER_PORT once the puter container is healthy."
log "first-boot admin password is logged once — grab it with:"
log " cd $PUTER_DIR && docker compose logs puter | grep tmp_password"