-
Notifications
You must be signed in to change notification settings - Fork 4k
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·231 lines (205 loc) · 9.87 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·231 lines (205 loc) · 9.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#!/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 + caddy/Caddyfile 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 Caddy (default: 80)
# PUTER_PROTOCOL public scheme: http | https (default: http)
# Set https once TLS is terminating in front of Puter —
# either the bundled Caddy with your certs (see "Step 3 —
# TLS" in doc/self-hosting.md) or your own proxy (Traefik,
# a cloud LB). Puter then builds https:// origins, S3
# URLs, and redirects. Leave it http and the installer
# serves plain HTTP on PUTER_PORT.
# PUTER_TRUST_PROXY number of reverse-proxy hops in front (default: 1)
# 1 = the bundled Caddy (or a single external proxy);
# 2 = two hops (e.g. Cloudflare → Caddy → Puter).
# PUTER_ENV prod | dev (default: prod)
# 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_PROTOCOL="${PUTER_PROTOCOL:-http}"
PUTER_TRUST_PROXY="${PUTER_TRUST_PROXY:-1}"
PUTER_ENV="${PUTER_ENV:-prod}"
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
# Pre-create per-service data dirs and make them writable by any UID.
# Several upstream images run as non-root inside the container (rustfs
# uses UID 10001; dynamo is pinned to 1000 in compose), and rustfs's
# entrypoint runs as that same non-root user so it can't chown an
# already-existing bind-mounted dir. On hosts where the user that ran
# this script has a UID that doesn't match — or where docker is running
# rootless — those containers loop on EACCES at startup. 0777 on the
# bind-mount roots sidesteps the mismatch without guessing each image's
# internal UID. (Docker Desktop on macOS/Windows papers over this with
# its VM layer; native Linux docker on Debian/Alpine doesn't.)
mkdir -p puter/data/valkey puter/data/mariadb puter/data/dynamo puter/data/s3 puter/data/puter puter/data/caddy
chmod 0777 puter/data/valkey puter/data/mariadb puter/data/dynamo puter/data/s3 puter/data/puter puter/data/caddy
log "install dir: $(pwd)"
# ── Step 3: docker-compose.yml + Caddy config ──────────────────────
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"
# Caddy is mounted as `./caddy/Caddyfile:/etc/caddy/Caddyfile:ro,z` — if
# the host file is missing, docker silently creates a directory at that
# path and the mount fails with "not a directory" at container start.
# The Caddyfile is domain-agnostic — it answers on every Host and leaves
# the subdomain routing to Puter — so there's nothing to template in.
log "downloading caddy/Caddyfile from $PUTER_URL"
mkdir -p caddy
# If the path was previously auto-created as a dir by a failed `compose up`,
# remove it so curl can write the file.
[ -d caddy/Caddyfile ] && rmdir caddy/Caddyfile 2>/dev/null || true
curl -fsSL "$PUTER_URL/caddy/Caddyfile" -o caddy/Caddyfile \
|| die "could not fetch $PUTER_URL/caddy/Caddyfile"
# ── 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)
# Two JWT secrets: `jwt_secret` is verify-only for legacy v1 tokens
# already in circulation; `jwt_secret_v2` signs every new token.
# Both are required at boot (`jwt_secret` only when verifying v1).
JWT_SECRET=$(openssl rand -hex 64)
JWT_SECRET_V2=$(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 in caddy/Caddyfile
# # (see "Step 3 — TLS" in doc/self-hosting.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
# `meteringEnforcement.subscriptions` is off here because a self-hosted
# install has no paid plans to be on: every account resolves to the free
# policy, so leaving the plan gates armed would 402 the surfaces that
# declare one (the OpenAI/Anthropic-compatible AI endpoints) for everybody.
# The code default is on — this is a property of the install, not of Puter.
log "writing puter/config/config.json"
cat > puter/config/config.json <<EOF
{
"domain": "$PUTER_DOMAIN",
"protocol": "$PUTER_PROTOCOL",
"pub_port": $PUTER_PORT,
"env": "$PUTER_ENV",
"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",
"jwt_secret_v2": "$JWT_SECRET_V2",
"allow_v1_tokens": true,
"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": "$PUTER_PROTOCOL://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 }
},
"meteringEnforcement": {
"subscriptions": false
},
"trust_proxy": $PUTER_TRUST_PROXY
}
EOF
fi
# `https` only works if something in front is actually terminating it. The
# bundled Caddy does that from ./puter/tls once you enable its `:443` block
# (see "Step 3 — TLS" in doc/self-hosting.md); your own edge proxy is fine
# too. Neither in place means Puter hands out https:// URLs for an endpoint
# that only speaks HTTP — broken logins and mixed content.
if [ "$PUTER_PROTOCOL" = "https" ] && [ ! -f puter/tls/fullchain.pem ]; then
warn "PUTER_PROTOCOL=https but ./puter/tls/fullchain.pem is missing."
warn "The bundled Caddy is still serving plain HTTP on port $PUTER_PORT."
warn "Either drop a wildcard cert into ./puter/tls and uncomment the :443"
warn "block in caddy/Caddyfile, or terminate TLS in your own proxy."
fi
# ── Step 5: bring it up ─────────────────────────────────────────────
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 $PUTER_PROTOCOL://$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 password"