Files
puter/doc/selfhosting/docker.md
T
Daniel Salazar a24246bfbc selfhosting: p3 cleanup docs + small fixes again (#2904)
* fix: Perms and configs

* rm broken editor for now

* selfhosting cleanup
2026-05-04 18:47:37 -07:00

183 lines
5.8 KiB
Markdown

# 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.