mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 15:37:12 +00:00
docker-compose from index (#2909)
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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 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 <service>` 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 <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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
Reference in New Issue
Block a user