Layers

Self-hosted

Layers on your server

One app and a few services in one docker-compose.yml. Installs in an air-gapped network too; your data never leaves your infrastructure.

The hissih/layers image on Docker Hub — over 20,000 pulls. 90-day trial, then a license is required.

God Mode → Getting started: server checklist, 4 of 6 done — domains, email, modules, AI

Install with AI

Install it with an AI assistant

Paste the prompt into ChatGPT, Claude or any other assistant. It reads the full guide and walks you through it: domain, email, calls, editors, AI model.

Guide for AI and humans: layers.md/self-hosted/llms.txt

Prompt
Help me install Layers on my server. Guide: https://layers.md/self-hosted/llms.txt (full version: https://layers.md/self-hosted/llms-full.txt). Walk me through it step by step and first ask about the domain, email and AI model.

What it is made of

One app and a few services next to it

Everything goes in one docker-compose.yml. Only Layers and PostgreSQL are required — add the rest as you need it.

Required

Layers

8080

The whole workspace, API and the God Mode admin panel — one Java app, one port.

Required

PostgreSQL

5432

Database. Bundled in compose, or your own external server.

Recommended

Search: Manticore

9308

Search across tasks, pages, chats and files. Manticore is a separate lightweight container, already in compose. Already run an Elasticsearch cluster? Layers works with it too.

Without a separate service — built-in Lucene, index in the file directory.

For calls

LiveKit

7880 · 7881 · UDP 50000–50100

Calls and screen sharing. Recording needs the LiveKit Egress service, installed separately.

Without it — no calls; chats still work.

For email

SMTP

587 / 465

Your mail server: invitations, sign-up confirmation, password reset.

Without it — no emails go out.

For documents

SumDoc

8090 · /sumdoc

Edit .docx in the browser.

Without it — documents open as a download card.

For spreadsheets

SumSheet

8092 · /sumsheet

Edit .xlsx and .xlsm in the browser.

Without it — spreadsheets open as a download card.

Optional

MCP server

8091 · /mcp

The assistant and external agents (Claude, Cursor) work with tasks and pages.

Without it — the assistant only answers, no actions.

Optional

LLM

OpenAI-compatible /v1

Your model: Ollama or any OpenAI-compatible server, or a cloud provider by key.

Without it — no AI features.

Linux x86-64 + Docker

Images are built for linux/amd64. Docker and Docker Compose.

Layers: 2 GB RAM

Minimum for the app, plus PostgreSQL.

Editors: 2 GB + ~250 MB

per open document. Each document runs in its own process.

Files up to 900 MB

by default. The limit is changed with a variable.

Install

From a blank server to the first sign-in

Thirteen steps, each with ready code. Passwords and keys in the examples are change-me placeholders — replace them.

  1. 1

    Prepare the server

    Linux x86-64 with Docker and Docker Compose. A domain pointing at the server, e.g. example.com. Inside a private network you can use http://IP:8080 instead.

    Generate your own password for every change-me placeholder.

    Terminal
    docker --version
    docker compose version
    mkdir -p /opt/layers && cd /opt/layers
    openssl rand -hex 24
  2. 2

    Create docker-compose.yml

    One file for the whole install. Replace example.com with your domain and every change-me-… with your own value. Drop the services you don’t need — calls, editors, MCP — together with their variables.

    The Layers volume must be /root/layers_data: files, God Mode settings and secrets live there. The database password appears twice and must match; so does the LiveKit key.

    Layers and editor images are on Docker Hub: hissih/layers.

    docker-compose.yml
    name: layers
    
    services:
      db:
        image: postgres:16-alpine
        restart: unless-stopped
        environment:
          POSTGRES_DB: layers
          POSTGRES_USER: layers
          POSTGRES_PASSWORD: change-me-db
        volumes:
          - pg_data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U layers -d layers"]
          interval: 5s
          retries: 20
    
      manticore:
        image: manticoresearch/manticore:latest
        restart: unless-stopped
        volumes:
          - manticore_data:/var/lib/manticore
    
      layers:
        image: hissih/layers:latest
        platform: linux/amd64
        restart: unless-stopped
        depends_on:
          db: { condition: service_healthy }
          manticore: { condition: service_started }
        ports:
          - "127.0.0.1:8080:8080"
        volumes:
          - layers_data:/root/layers_data
        environment:
          SPRING_PROFILES_ACTIVE: server
          # Database
          DB_SERVER_URL: jdbc:postgresql://db:5432/layers
          DB_SERVER_USERNAME: layers
          DB_SERVER_PASSWORD: change-me-db
          # Admin panel /godmode
          ADMIN_USERNAME: admin
          ADMIN_PASSWORD: change-me-admin
          # Domain
          LAYERS_DOMAIN: example.com
          LAYERS_APP_DOMAIN: example.com
          APP_REDIRECT_URL: https://example.com
          APP_CORS_ALLOWED_ORIGINS: https://example.com
          # Search
          SEARCH_PROVIDER: manticore
          SEARCH_MANTICORE_ENABLED: "true"
          SEARCH_MANTICORE_URL: http://manticore:9308
          SEARCH_ELASTICSEARCH_ENABLED: "false"
          # Calls
          LIVEKIT_ENABLED: "true"
          LIVEKIT_URL: ws://livekit:7880
          LIVEKIT_PUBLIC_URL: wss://example.com/livekit/
          LIVEKIT_API_KEY: change-me-livekit-key
          LIVEKIT_API_SECRET: change-me-livekit-secret-min-32-characters
          # Email (outgoing)
          MAIL_PROVIDER: smtp
          SPRING_MAIL_HOST: smtp.example.com
          SPRING_MAIL_PORT: "587"
          SPRING_MAIL_USERNAME: noreply@example.com
          SPRING_MAIL_PASSWORD: change-me-smtp
          SPRING_MAIL_SMTP_AUTH: "true"
          SPRING_MAIL_SMTP_STARTTLS_ENABLE: "true"
          # SumDoc and SumSheet editors (addresses the browser sees)
          LAYERS_FASTDOC_URL: https://example.com/sumdoc
          LAYERS_FASTSHEET_URL: https://example.com/sumsheet
          # AI: the model is connected in God Mode → AI
          AI_ENABLED: "true"
          ASSISTANT_MCP_URL: http://mcp:8091/mcp
          AI_USER_MODEL_CHOICE: "true"
    
      livekit:
        image: livekit/livekit-server:latest
        restart: unless-stopped
        environment:
          LIVEKIT_CONFIG: |
            port: 7880
            bind_addresses: [""]
            rtc:
              tcp_port: 7881
              port_range_start: 50000
              port_range_end: 50100
              use_external_ip: true
            keys:
              change-me-livekit-key: change-me-livekit-secret-min-32-characters
            webhook:
              api_key: change-me-livekit-key
              urls:
                - http://layers:8080/v1/livekit/webhook
        ports:
          - "7880:7880"
          - "7881:7881"
          - "7882:7882/udp"
          - "50000-50100:50000-50100/udp"
    
      sumdoc:
        image: hissih/sumdoc-webhost:latest
        platform: linux/amd64
        restart: unless-stopped
        command: ["--base-path", "/sumdoc", "--max-cabins", "12", "--idle-min", "20",
                  "--auth-url", "http://layers:8080/v1/me",
                  "--attach-url", "http://layers:8080/v1/attachments/{id}/content"]
        ports:
          - "127.0.0.1:8090:8090"
        volumes:
          - sumdoc_cabins:/data/cabins
    
      sumsheet:
        image: hissih/sumsheet-webhost:latest
        platform: linux/amd64
        restart: unless-stopped
        command: ["--base-path", "/sumsheet", "--max-cabins", "12", "--idle-min", "20",
                  "--auth-url", "http://layers:8080/v1/me",
                  "--attach-url", "http://layers:8080/v1/attachments/{id}/content"]
        ports:
          - "127.0.0.1:8092:8092"
        volumes:
          - sumsheet_cabins:/data/cabins
    
      mcp:
        image: layers-mcp-server:from-distribution   # MCP server image from the distribution
        restart: unless-stopped
        depends_on: [layers]
        environment:
          SPRING_PROFILES_ACTIVE: server
          SERVER_PORT: "8091"
          LAYERS_BACKEND_BASEURL: http://layers:8080
          LAYERS_BACKEND_FORWARDINCOMINGAUTH: "true"
          LAYERS_BACKEND_REQUIREINCOMINGAUTH: "true"
        ports:
          - "127.0.0.1:8091:8091"
    
    volumes:
      pg_data:
      manticore_data:
      layers_data:
      sumdoc_cabins:
      sumsheet_cabins:
  3. 3

    Choose the search engine

    Layers ships three search engines, picked with SEARCH_PROVIDER (auto, manticore, elasticsearch, lucene).

    Manticore — recommended: a separate lightweight container, already in compose. Elasticsearch — if your company already runs a cluster: remove the manticore service and set the variables on the right. Lucene — built in, no separate service: the index lives in the Layers file directory (the layers_data volume); fits a small install.

    After switching engines, reindex: God Mode → Platform → Search.

    Elasticsearch — instead of the Manticore variables on layers
          SEARCH_PROVIDER: elasticsearch
          SEARCH_ELASTICSEARCH_ENABLED: "true"
          SEARCH_MANTICORE_ENABLED: "false"
          ES_HOST: elasticsearch.example.com
          ES_PORT: "9200"
          ES_PROTOCOL: https
          ES_USERNAME: layers
          ES_PASSWORD: change-me-es
    Lucene — built in, no manticore service
          SEARCH_PROVIDER: lucene
          SEARCH_MANTICORE_ENABLED: "false"
          SEARCH_ELASTICSEARCH_ENABLED: "false"
  4. 4

    Start it

    The first start takes 15–60 seconds. When the log shows Started LayersApplication, open your domain and create the first account.

    Terminal
    docker compose up -d
    docker compose logs -f layers
    curl -s http://127.0.0.1:8080/health
    # OK
  5. 5

    Turn on HTTPS with nginx

    Layers serves everything from port 8080 — nginx only terminates TLS and proxies calls, editors and MCP. Editors stay on the same domain so they receive the sign-in cookie.

    No limit_req and no static serving in nginx — brute-force protection and caching are built in.

    Terminal
    sudo apt install nginx certbot python3-certbot-nginx
    sudo certbot certonly --nginx -d example.com
    /etc/nginx/sites-available/layers.conf
    server {
        listen 80;
        server_name example.com;
        location /.well-known/acme-challenge/ { root /var/www/certbot; }
        location / { return 301 https://example.com$request_uri; }
    }
    
    server {
        listen 443 ssl http2;
        server_name example.com;
    
        ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
        client_max_body_size 1000m;
        add_header X-Content-Type-Options nosniff always;
        add_header X-Frame-Options SAMEORIGIN always;
    
        # Layers: web, API, God Mode
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
            proxy_read_timeout 300;
        }
    
        # LiveKit: signalling (the trailing slash in proxy_pass matters)
        location /livekit/ {
            proxy_pass http://127.0.0.1:7880/;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 86400s;
        }
        # LiveKit REST: the server SDK calls /twirp/ on the root
        location /twirp/ {
            proxy_pass http://127.0.0.1:7880/twirp/;
            proxy_set_header Host $host;
            proxy_read_timeout 86400s;
        }
    
        # Editors — on the same domain, so they get the Layers sign-in cookie
        location /sumdoc {
            proxy_pass http://127.0.0.1:8090;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 3600s;
        }
        location /sumsheet {
            proxy_pass http://127.0.0.1:8092;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 3600s;
        }
    
        # MCP for external agents (optional)
        location /mcp {
            proxy_pass http://127.0.0.1:8091/mcp;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_buffering off;
            proxy_read_timeout 3600s;
        }
    }
    Terminal
    sudo ln -s /etc/nginx/sites-available/layers.conf /etc/nginx/sites-enabled/
    sudo rm -f /etc/nginx/sites-enabled/default
    sudo nginx -t && sudo systemctl reload nginx
  6. 6

    Open the ports for calls

    LiveKit carries audio and video over UDP. In God Mode → Apps → Integrations → LiveKit set wss://example.com/livekit/ (trailing slash required), test the connection and enable it.

    If the log says could not validate external IP, replace use_external_ip: true with node_ip: <server IP>. Use node_ip in a closed network too.

    Terminal
    sudo ufw allow 7881/tcp
    sudo ufw allow 7882/udp
    sudo ufw allow 50000:50100/udp
  7. 7

    Set up email

    Emails from Layers — invitations, confirmations, password resets — go out through your SMTP server from compose. The same settings live in God Mode → Platform → Email, with a test email and templates.

    The Mail module is a client inside Layers: people connect their existing mailboxes over IMAP and SMTP. Layers does not run a mail server of its own.

    Port 465 instead of 587
    SPRING_MAIL_PORT: "465"
    SPRING_MAIL_SMTP_STARTTLS_ENABLE: "false"
    SPRING_MAIL_PROPERTIES_MAIL_SMTP_SSL_ENABLE: "true"
  8. 8

    Check the SumDoc and SumSheet editors

    The editor stores no users and no files: it asks Layers who the person is (/v1/me) and fetches the file by id (/v1/attachments). skewMin: 0 means editor and engine come from the same release.

    One open document takes about 250 MB of RAM; --max-cabins sets how many can be open at once.

    Terminal
    curl -s http://127.0.0.1:8090/sumdoc/health     # "status": "ok", "skewMin": 0
    curl -s http://127.0.0.1:8092/sumsheet/health   # "status": "ok", "skewMin": 0
    curl -s https://example.com/v1/capabilities
    # {"fastdoc":{"enabled":true,"url":"https://example.com/sumdoc"},
    #  "fastsheet":{"enabled":true,"url":"https://example.com/sumsheet"}, …}
  9. 9

    Connect an AI model

    God Mode → AI → Providers → Add provider. Built-in types are OpenAI, OpenRouter and Gonka; Custom connection takes any server with an OpenAI-compatible API, such as Ollama, no key required. “Test” lists the server’s models — pick the ones you want and enable them.

    Our test setup uses a local Ollama with gpt-oss:20b this way. The address must be reachable from the Layers container: a service name or IP, not localhost.

    docker-compose.yml — add under services
      ollama:
        image: ollama/ollama:latest
        restart: unless-stopped
        volumes:
          - ollama_data:/root/.ollama
    # and under volumes:
    #   ollama_data:
    Terminal
    docker compose up -d ollama
    docker compose exec ollama ollama pull gpt-oss:20b
    # address for “Custom connection”: http://ollama:11434/v1
    # OpenAI-compatible server on another machine: http://<host>:8000/v1
  10. 10

    Connect agents over MCP

    The MCP server is already in compose: through it the Layers assistant takes actions — creates tasks, writes pages. External agents (Claude, Cursor and others) connect to https://example.com/mcp with a personal token: in Layers → Settings → Access Tokens, read-only or read and write.

    Terminal
    curl -s http://127.0.0.1:8091/actuator/health
    # {"status":"UP", …}
  11. 11

    Sign in to God Mode

    https://example.com/godmode/login, user admin and your ADMIN_PASSWORD. “Getting started” is the server checklist: domains, email, sign-in, modules, branding. Each item is marked done from the server’s actual state.

    God Mode → Getting started: setup steps — domains, email, sign-in, modules, branding, AI
  12. 12

    Enter the license key

    The trial period is 90 days; after that a license is required. The key is one line: God Mode → System → License → “Apply key”. It is verified on the server itself.

    Terminal
    curl -s https://example.com/layers/license/status
    # {"mode":"MISSING", …, "node":{"regime":"evaluation","evaluationDaysLeft":89, …}}
    # with a key: "regime":"full"
  13. 13

    Update and back up

    Updating means a new image; the database, files and settings stay in the volumes. Pin a tag instead of latest for predictable updates — the running tag is shown in God Mode → Operations → System health.

    There is no built-in database backup in the panel: back up the database with pg_dump, files and settings as an archive of the layers_data volume.

    Update
    cd /opt/layers
    docker compose pull
    docker compose up -d
    Backup
    cd /opt/layers
    docker compose exec -T db sh -c 'pg_dump -U $POSTGRES_USER $POSTGRES_DB' | gzip > layers-db-$(date +%F).sql.gz
    docker run --rm -v layers_layers_data:/data -v "$PWD":/backup alpine \
      tar czf /backup/layers-data-$(date +%F).tar.gz -C /data .

Air-gapped

Air-gapped install — images as an archive

Pull the images on a machine with internet access and carry them over as one file. The license is checked on the server itself; the module catalog reaches out to the registry, and inside a closed network AI works with a local model only.

Terminal
docker pull hissih/layers:latest
docker save hissih/layers:latest postgres:16-alpine manticoresearch/manticore:latest \
  livekit/livekit-server:latest hissih/sumdoc-webhost:latest hissih/sumsheet-webhost:latest \
  | gzip > layers-images.tar.gz
# on the server inside the closed network
gunzip -c layers-images.tar.gz | docker load
docker compose up -d

Tested on our setup

How it runs for us

Every screenshot and clip on this site comes from a local test setup running exactly these services. Ports are shifted to 18xxx to avoid clashes; Mailpit and GreenMail are there only to test email.

ServiceImagePort in our setupOn your server
Layershissih/layers 2.1.184 (same jar)180808080 behind nginx
PostgreSQLpostgres:16-alpine18432db:5432, internal
Manticoremanticoresearch/manticore18308manticore:9308, internal
LiveKitlivekit/livekit-server18880 · 18881 · UDP 18900–189507880 · 7881 · UDP 50000–50100
Mailpit (mail trap)axllent/mailpitSMTP 18125your SMTP server
GreenMail (test mailboxes)greenmail/standalone:2.1.3IMAP 18143 · SMTP 18325people’s existing mailboxes
SumDocsumdoc-webhost18290 · /sumdoc8090 · /sumdoc
SumSheetsumsheet-webhost18292 · /sumsheet8092 · /sumsheet
MCP serverfrom the distribution18091 · /mcp8091 · /mcp
LLMOllama, gpt-oss:20b11434 · /v1your model or provider

Checked on 2 October 2026: /health → OK, license — trial (89 days left), editors — status ok, MCP — UP.

Why this way

Control without compromise

Closed network

Installs in an air-gapped network: images and updates travel as an archive.

License on the server

The key is verified on the server itself.

No lock-out by headcount

The server never blocks people above the license size. Storage — as much as your hardware holds; per-space quotas are yours to set.

Your brand

Name, logo, colour, default language and your own legal documents.

Your AI

OpenAI, OpenRouter, any OpenAI-compatible server or local models.

Editors next to it

SumDoc and SumSheet run beside Layers, in the air-gapped network too.

License

Pricing on request

Trial

90 days

90 days at full speed. Without a key the server keeps running: people and data are not limited, only meeting transcription runs with a delay.

License

On request

We’ll discuss your team size, support and license terms.

Discuss self-hosting

The server never blocks people beyond the license. Setup, rollout, updates, customization and support are separate, by agreement and paid.

Frequently asked questions

Do I need internet?

No — not to install from an archive, not to run, not to check the license. AI in a closed network works with a local model.

How much hardware?

Layers needs at least 2 GB of RAM plus PostgreSQL. Editors need 2 GB plus about 250 MB per open document. Images are linux/amd64.

How do I update?

docker compose pull and docker compose up -d. Data and settings stay in the volumes.

How do I back up?

The database with pg_dump, files and settings as an archive of the layers_data volume. There is no built-in database backup in the panel.

A call connects but there is no audio or video

UDP 50000–50100 or 7882 is closed on the firewall, or LiveKit cannot find its external IP — set node_ip.

What happens after 90 days without a key?

The server keeps working: sign-in, people and data are not limited, nothing is closed. Only meeting transcription slows down — it runs at low priority with a few seconds of delay. With a key everything is back to full speed.

Is there corporate directory sign-in?

Not yet: SAML, LDAP and SCIM are not supported. Sign-in with password, Google, Yandex, VK and Gosuslugi are available.

Bring your team’s work together in Layers

Start free in the cloud — up to 20 people, no time limit — or deploy Layers on your own server.