puter Documentation

The Internet Computer! Free, Open-Source, and Self-Hostable.

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy puter on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name puter -p 25:25 --restart always ghcr.io/heyputer/puter:main
Configured Services & Ports
⚙️ valkey
image: valkey/valkey:8-alpine
⚙️ mariadb
image: mariadb:11
⚙️ dynamo
image: amazon/dynamodb-local:latest
⚙️ s3
image: rustfs/rustfs:latest
⚙️ s3-init
image: amazon/aws-cli:latest
⚙️ ollama
image: ollama/ollama:latest
⚙️ ollama-init
image: ollama/ollama:latest
⚙️ puter
image: ghcr.io/heyputer/puter:main
⚙️ puter-smtp
image: ghcr.io/heyputer/puter:main
🔌 Ports: ${SMTP_PORT:-25}:2525
⚙️ caddy
image: caddy:2.11-alpine
🔌 Ports: ${HTTP_PORT:-80}:80
docker-compose.yml
yaml (docker-compose.yml)
---
# Self-hosted Puter — full stack.
#
# Brings up Puter + every external service it needs:
#   - caddy    : 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.
#   - Enable TLS on the bundled Caddy (see caddy/Caddyfile) or front the
#     stack with your own TLS-terminating proxy.
#   - 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 Caddy (`s3.`),
    # 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 and
    # applies its CORS rules on every boot (put-bucket-cors is
    # idempotent). Without CORS, browser uploads to presigned URLs
    # fail the preflight check.
    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
        # Browser uploads (Dev Center deploy, puter.fs.upload) PUT to
        # presigned URLs cross-origin, so the bucket must answer the
        # OPTIONS preflight. Origins are open because the presigned URL
        # itself is the credential and the SDK sends no cookies.
        cat > /tmp/cors.json <<'EOF'
        {
          "CORSRules": [
            {
              "AllowedOrigins": ["*"],
              "AllowedMethods": ["GET", "HEAD", "PUT", "POST", "DELETE"],
              "AllowedHeaders": ["*"],
              "ExposeHeaders": ["ETag", "x-amz-request-id"],
              "MaxAgeSeconds": 3600
            }
          ]
        }
        EOF
        aws --endpoint-url "$$endpoint" s3api put-bucket-cors \
          --bucket "$$bucket" --cors-configuration file:///tmp/cors.json
        echo "bucket CORS configured"
    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
    # For local builds, create a docker-compose.override.yml;
    # see doc/self-hosting.md#building-from-source-instead-of-pulling.
    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: Caddy 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://localhost:4100/ || exit 1
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 30s

  # Inbound mail. Behind the `smtp` profile, so it only starts when asked for:
  #   docker compose --profile smtp up -d
  # Accepts SMTP from other mail servers and hands each message to the ingress
  # endpoint, which files it into the recipient's mailbox. Needs `userEmail`
  # configured — see doc/self-hosting.md#optional-receiving-mail-over-smtp.
  puter-smtp:
    profiles: ["smtp"]
    image: ghcr.io/heyputer/puter:main
    pull_policy: always
    container_name: puter-smtp
    restart: unless-stopped
    command: ["node", "-r", "./dist/src/backend/telemetry.js", "./dist/src/backend/smtp/index.js"]
    depends_on:
      puter:
        condition: service_healthy
    ports:
      # The container runs unprivileged and binds 2525; 25 is what an MX record
      # means, and many providers block it.
      - "${SMTP_PORT:-25}:2525"
    environment:
      PUID: 1000
      PGID: 1000
    volumes:
      # The same config.json the backend reads; only `userEmail` applies here.
      - ./puter/config:/etc/puter:z

  caddy:
    image: caddy:2.11-alpine
    container_name: puter-caddy
    restart: unless-stopped
    depends_on:
      puter:
        condition: service_started
    ports:
      - "${HTTP_PORT:-80}:80"
      # Uncomment when you enable TLS in caddy/Caddyfile:
      # - "${HTTPS_PORT:-443}:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro,z
      # TLS certs (wildcard fullchain.pem + privkey.pem). Read-only inside.
      - ./puter/tls:/etc/caddy/tls:ro,z
      # Caddy's own state — the cert store it would use if you swap this
      # config over to ACME. Kept under ./puter/data with everything else
      # so one directory is the whole backup.
      - ./puter/data/caddy:/data:z
    healthcheck:
      # Hits Caddy's local-only admin API rather than proxying through to
      # Puter: this reports whether the proxy itself is up and configured,
      # and keeps working once the `:80` block becomes an HTTPS redirect.
      # 127.0.0.1, not localhost — the admin endpoint is IPv4-only and
      # busybox wget tries ::1 first.
      test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://127.0.0.1:2019/config/ >/dev/null || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 5s

1. Project Overview & Quickstart (HeyPuter/puter)

The Open-Source Internet Computer!

Puter

Puter is an advanced, open-source, self-hostable internet computer designed to be feature-rich, fast, and highly extensible.

For Users

Puter's goal is to provide you with every app and feature you need to work, create, and play under one roof. From a simple Notepad and Voice Recorder to Spreadsheet and Camera, Puter wants to be the all-in-one solution for your digital life.

For Developers

Puter provides everything you need to build and publish web apps and games. From AI to Cloud Storage and Database to Serverless Workers, Puter has you covered. Puter also helps you get users! Once you build your app, you can publish it on our App Store to reach and monetize users.

Getting Started

💻 Local Development

bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install
npm start

→ This should launch Puter at http://puter.localhost:4100

To run this checkout with Docker, follow Building from source. Create a local docker-compose.override.yml to select the local build; keeping these settings out of docker-compose.yml avoids conflicts when pulling updates and keeps local configuration out of pull requests.

🚀 Self-Hosting

Linux/macOS

bash
curl -fsSL https://puter.com/selfhost | sh

Windows

powershell
irm https://puter.com/selfhost?os=windows | iex

→ For more details, see Self-Hosting Puter.

☁️ Puter.com

Puter is available as a hosted service at puter.com.

Support

Connect with the maintainers and community through these channels:

We are always happy to help you with any questions you may have. Don't hesitate to ask!

License

This repository, including all its contents, sub-projects, modules, and components, is licensed under AGPL-3.0 unless explicitly stated otherwise. Third-party libraries included in this repository may be subject to their own licenses.

Translations

2. Official Technical Reference & Guides (HeyPuter/docs)

[!IMPORTANT]
The Puter.js documentation has moved to the main Puter repository at src/docs.

Please submit all contributions and issues to the main repo instead, as this repository is now archived.

Puter.js Docs

Puter.js Docs

The Puter.js documentation contains everything you need to build powerful applications with Puter.js.

  • Get started with Puter.js by reading documentations on usage and best practices
  • Browse all available APIs, including AI, networking, authentication, and cloud services
  • Find code examples and implementations to speed up your development

Getting Started

💻 Local Development

bash
git clone https://github.com/HeyPuter/docs
cd docs
npm install
npm run dev

→ This should launch Puter.js Docs at
http://127.0.0.1:8080 (or the next available port).

Support

Connect with the maintainers and community through these channels:

We are always happy to help you with any questions you may have. Don't hesitate to ask!

License

This repository, including its sub-projects, modules, and components, is licensed under MIT, and its content is licensed under CC BY-SA 4.0 unless explicitly stated otherwise. Third-party libraries included in this repository may be subject to their own licenses.