mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-27 11:55:50 +00:00
294 lines
10 KiB
YAML
294 lines
10 KiB
YAML
---
|
|
# 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:
|
|
# `:z` is an SELinux relabel hint for Fedora/RHEL hosts (no-op
|
|
# everywhere else) — without it those distros deny container
|
|
# access to the bind mount and the service loops on EACCES.
|
|
- ./puter/data/valkey:/data:z
|
|
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:z
|
|
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:z
|
|
|
|
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:z
|
|
# 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:z
|
|
# 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:main
|
|
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:z
|
|
# Persistent runtime data (anything your config points at /var/puter).
|
|
- ./puter/data/puter:/var/puter:z
|
|
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,z
|
|
# TLS certs (fullchain.pem + privkey.pem). Read-only inside.
|
|
- ./puter/tls:/etc/nginx/tls:ro,z
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://localhost/ || exit 1"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 5
|
|
start_period: 5s
|