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.
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
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.
Layers
8080
The whole workspace, API and the God Mode admin panel — one Java app, one port.
PostgreSQL
5432
Database. Bundled in compose, or your own external server.
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.
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.
SMTP
587 / 465
Your mail server: invitations, sign-up confirmation, password reset.
Without it — no emails go out.
SumDoc
8090 · /sumdoc
Edit .docx in the browser.
Without it — documents open as a download card.
SumSheet
8092 · /sumsheet
Edit .xlsx and .xlsm in the browser.
Without it — spreadsheets open as a download card.
MCP server
8091 · /mcp
The assistant and external agents (Claude, Cursor) work with tasks and pages.
Without it — the assistant only answers, no actions.
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
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 usehttp://IP:8080instead.Generate your own password for every
change-meplaceholder.Terminaldocker --version docker compose version mkdir -p /opt/layers && cd /opt/layers openssl rand -hex 24 - 2
Create docker-compose.yml
One file for the whole install. Replace
example.comwith your domain and everychange-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.ymlname: 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
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
manticoreservice and set the variables on the right. Lucene — built in, no separate service: the index lives in the Layers file directory (thelayers_datavolume); fits a small install.After switching engines, reindex: God Mode → Platform → Search.
Elasticsearch — instead of the Manticore variables on layersSEARCH_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-esLucene — built in, no manticore serviceSEARCH_PROVIDER: lucene SEARCH_MANTICORE_ENABLED: "false" SEARCH_ELASTICSEARCH_ENABLED: "false" - 4
Start it
The first start takes 15–60 seconds. When the log shows
Started LayersApplication, open your domain and create the first account.Terminaldocker compose up -d docker compose logs -f layers curl -s http://127.0.0.1:8080/health # OK - 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_reqand no static serving in nginx — brute-force protection and caching are built in.Terminalsudo apt install nginx certbot python3-certbot-nginx sudo certbot certonly --nginx -d example.com/etc/nginx/sites-available/layers.confserver { 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; } }Terminalsudo 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
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, replaceuse_external_ip: truewithnode_ip: <server IP>. Usenode_ipin a closed network too.Terminalsudo ufw allow 7881/tcp sudo ufw allow 7882/udp sudo ufw allow 50000:50100/udp - 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 587SPRING_MAIL_PORT: "465" SPRING_MAIL_SMTP_STARTTLS_ENABLE: "false" SPRING_MAIL_PROPERTIES_MAIL_SMTP_SSL_ENABLE: "true" - 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: 0means editor and engine come from the same release.One open document takes about 250 MB of RAM;
--max-cabinssets how many can be open at once.Terminalcurl -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
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:20bthis way. The address must be reachable from the Layers container: a service name or IP, notlocalhost.docker-compose.yml — add under servicesollama: image: ollama/ollama:latest restart: unless-stopped volumes: - ollama_data:/root/.ollama # and under volumes: # ollama_data:Terminaldocker 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
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/mcpwith a personal token: in Layers → Settings → Access Tokens, read-only or read and write.Terminalcurl -s http://127.0.0.1:8091/actuator/health # {"status":"UP", …} - 11
Sign in to God Mode
https://example.com/godmode/login, useradminand yourADMIN_PASSWORD. “Getting started” is the server checklist: domains, email, sign-in, modules, branding. Each item is marked done from the server’s actual state. - 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.
Terminalcurl -s https://example.com/layers/license/status # {"mode":"MISSING", …, "node":{"regime":"evaluation","evaluationDaysLeft":89, …}} # with a key: "regime":"full" - 13
Update and back up
Updating means a new image; the database, files and settings stay in the volumes. Pin a tag instead of
latestfor 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 thelayers_datavolume.Updatecd /opt/layers docker compose pull docker compose up -dBackupcd /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.
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.
| Service | Image | Port in our setup | On your server |
|---|---|---|---|
| Layers | hissih/layers 2.1.184 (same jar) | 18080 | 8080 behind nginx |
| PostgreSQL | postgres:16-alpine | 18432 | db:5432, internal |
| Manticore | manticoresearch/manticore | 18308 | manticore:9308, internal |
| LiveKit | livekit/livekit-server | 18880 · 18881 · UDP 18900–18950 | 7880 · 7881 · UDP 50000–50100 |
| Mailpit (mail trap) | axllent/mailpit | SMTP 18125 | your SMTP server |
| GreenMail (test mailboxes) | greenmail/standalone:2.1.3 | IMAP 18143 · SMTP 18325 | people’s existing mailboxes |
| SumDoc | sumdoc-webhost | 18290 · /sumdoc | 8090 · /sumdoc |
| SumSheet | sumsheet-webhost | 18292 · /sumsheet | 8092 · /sumsheet |
| MCP server | from the distribution | 18091 · /mcp | 8091 · /mcp |
| LLM | Ollama, gpt-oss:20b | 11434 · /v1 | your 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.
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.
