Sign inSign up

hissih/layers

By hissih

Updated 1 day ago

Layers — flexible all-in-one project & knowledge management platform. Instant launch via Docker.

Image
Developer tools
4

10K+

hissih/layers repository overview

Layers — Team Workspace Platform

Self-hosted collaborative workspace for teams.


1. Requirements


2. Install

Create a new folder and save this as docker-compose.yml:

name: layers

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: layersdb
      POSTGRES_USER: layers
      POSTGRES_PASSWORD: changeme
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U layers -d layersdb"]
      interval: 2s
      timeout: 2s
      retries: 30
    volumes:
      - pg_data:/var/lib/postgresql/data

  layers:
    image: hissih/layers:latest
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    environment:
      # Database
      DB_SERVER_URL: jdbc:postgresql://db:5432/layersdb
      DB_SERVER_USERNAME: layers
      DB_SERVER_PASSWORD: changeme
      JPA_DDL_AUTO: update
      STABILITY_SERVER_JPA_DDL_GUARD_ENABLED: "false"
      # Admin panel (/godmode)
      ADMIN_USERNAME: admin
      ADMIN_PASSWORD: admin
      # Domain
      LAYERS_DOMAIN: localhost
      LAYERS_APP_DOMAIN: localhost
      APP_REDIRECT_URL: http://localhost:8080
      APP_CORS_ALLOWED_ORIGINS: http://localhost:8080
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - layers_data:/root/layers_data

  # Voice & video calls (LiveKit). Works out of the box — the application
  # already knows the default hostname/keys. For a public-facing install,
  # expose it over HTTPS (see "Voice & Video (LiveKit)" below).
  livekit:
    image: livekit/livekit-server:latest
    hostname: layers-livekit
    environment:
      LIVEKIT_CONFIG: |
        port: 7880
        bind_addresses: [""]
        rtc:
          tcp_port: 7881
          port_range_start: 50000
          port_range_end: 50100
          use_external_ip: true
        keys:
          layers_default_key: layers_default_secret_change_me_min_32_chars_xx
        webhook:
          api_key: layers_default_key
          urls:
            - http://layers:8080/v1/livekit/webhook
    ports:
      - "7880:7880"
      - "7881:7881"
      - "7882:7882/udp"
      - "50000-50100:50000-50100/udp"

volumes:
  pg_data:
  layers_data:

Run:

docker compose up -d

Wait ~15 seconds, then open:

At this point Layers is running. Everything below is optional.


3. Secure the defaults

Before making the app accessible to anyone else, change these values in docker-compose.yml:

WhatDefaultAction
POSTGRES_PASSWORD + DB_SERVER_PASSWORDchangemeSet a strong password (both must match)
ADMIN_PASSWORDadminSet a strong admin password
APP_JWT_SECRETauto-generatedAlready secure. Override only if you need a fixed value: openssl rand -base64 64

Then restart: docker compose up -d


4. Custom domain

If you access Layers from a domain (not localhost), update these 4 values:

LAYERS_DOMAIN: example.com
LAYERS_APP_DOMAIN: example.com
APP_REDIRECT_URL: https://example.com
APP_CORS_ALLOWED_ORIGINS: https://example.com

Then restart: docker compose up -d


5. HTTPS with Nginx

Layers serves everything — web UI, API, admin panel, static files — from a single application on port 8080. No separate web server is required.

ScenarioNginx needed?
Local network, VPN, or intranetNo — use http://server-ip:8080 directly
Development and testingNo
Behind an existing corporate proxyNo — point your proxy to port 8080
Public domain with HTTPSYes — Nginx terminates SSL, forwards to port 8080

If you don't need HTTPS, skip to the next section.

Setting up Nginx with HTTPS (click to expand)

Nginx only terminates SSL on 443 and forwards to 8080. Do not serve static files, cache or rate-limit in Nginx — the app does that itself.

Step 1. Install Nginx and Certbot
sudo apt install nginx certbot python3-certbot-nginx
Step 2. Get an SSL certificate
sudo certbot certonly --nginx -d example.com -d www.example.com
Step 3. Configure Nginx

Replace example.com with your domain. Add client_max_body_size 1000m; to http {} in /etc/nginx/nginx.conf (Nginx default 1m blocks uploads).

Site config/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;

    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300;
    }
}
Step 4. Enable and test
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
Step 5. Update Docker environment

Make sure your docker-compose.yml has the correct domain (see Step 4. Custom domain).

Important
  • Do not add limit_req — the SPA loads dozens of assets per page; rate limiting blocks the UI. Brute-force protection is built in.
  • Do not serve static files from Nginx — the app handles caching headers itself.
  • SSL auto-renewal: sudo certbot renew --dry-run

6. Update

Pull the new image and restart. Your data stays:

docker compose pull
docker compose up -d

Important: The volume must mount to /root/layers_data (not /data). All Godmode settings, uploaded files, JWT secrets, and email templates are stored there. If the volume path is wrong, settings will be lost on every update.

New services in newer versions

docker compose pull updates images only. When a release adds a service (e.g. livekit), add its block from the template above, then:

docker compose up -d

Settings, database and files stay in the layers_data and pg_data volumes.


7. Full Reset

Stop everything and delete all data (database + files):

docker compose down -v
docker compose up -d

License key

People are never limited: seats are not capped, data is never touched. Without a key a node runs 90 days at full speed; then the machine paths (transcription, export, conversion) slow down — people keep working, nothing is blocked, nothing phones home; the key is verified on your node, offline. Godmode → System → License shows the mode, the day counter and the key field; status also at GET /layers/license/status. Plans: 150 € / month up to 50 people, 400 € up to 200sumoffice.com/licensing.

Office editors (SumDoc, SumSheet)

Word and Excel files open inside Layers in SumDoc and SumSheet — the engines behind sumoffice.com: a .docx as pages in seconds, an .xlsx recalculated without Excel, macros and Power Query included. The editor service comes with the node license; see it on app.layers.md first.

Optional Features

Everything below is configured via environment variables. You only need to add the variables you want — everything has sensible defaults.

Voice & Video (LiveKit)

The livekit service in the install template runs on localhost out of the box — start a call, it works.

For a public install behind HTTPS, add two location blocks to your Nginx vhost (the LiveKit SDK strips prefixes from REST calls, sending them to /twirp/... on the root):

# WebSocket signaling
location /livekit/ {
    proxy_pass http://127.0.0.1:7880/;   # trailing slash matters
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 86400s;
}

# LiveKit REST API — backend SDK hits /twirp/ on root, not under /livekit/
location /twirp/ {
    proxy_pass http://127.0.0.1:7880/twirp/;
    proxy_set_header Host $host;
    proxy_read_timeout 86400s;
}

Open the media ports on the host firewall:

ufw allow 7881/tcp
ufw allow 50000:50100/udp

In Godmode → Integrations → LiveKit:

  • WebSocket URLwss://your-domain.com/livekit/ (trailing slash is mandatory — backend throws baseUrl must end in / without it).
  • API Key / Secret → keep the baked defaults or generate your own:
    openssl rand -hex 16   # new API key
    openssl rand -hex 24   # new secret (must be ≥32 chars)
    
    If changed, update both LIVEKIT_CONFIG in compose and Godmode fields.

Then: SaveTest connection (green toast Connected, 0 active rooms) → Enable LiveKit.

Alternative: dedicated subdomain kit.example.com → separate Nginx vhost, single location / proxying to 127.0.0.1:7880, URL wss://kit.example.com/ (no /twirp/ needed).

Diagnostics: GET /v1/livekit/status, Test connection in Godmode, HTTP 401 with matching keys → /twirp/ location missing.

Hosting gotchas (VPS with restrictive kernels/firewalls):

  • could not validate external IP — outbound UDP to STUN is blocked: replace use_external_ip: true with node_ip: <public-ip> in LIVEKIT_CONFIG.rtc.
  • curl http://127.0.0.1:7880 returns "Connection refused" while the container runs — use "7880:7880" instead of "127.0.0.1:7880:7880" in port mapping, or enable: echo "net.ipv4.conf.all.route_localnet=1" | sudo tee /etc/sysctl.d/99-docker-localnet.conf && sudo sysctl --system
Email (SMTP / SendGrid)

Without email there are no registration confirmations, password resets or invitations.

SMTP (Gmail, Mailgun, any SMTP server):

# Add to layers service environment:
MAIL_PROVIDER: smtp                           # Enable SMTP email
SPRING_MAIL_HOST: smtp.gmail.com              # Your SMTP server
SPRING_MAIL_PORT: "587"                       # SMTP port (587 for STARTTLS, 465 for SSL)
SPRING_MAIL_USERNAME: [email protected]     # SMTP login
SPRING_MAIL_PASSWORD: your-app-password       # SMTP password (use app-specific password for Gmail)
SPRING_MAIL_SMTP_AUTH: "true"                 # Enable authentication
SPRING_MAIL_SMTP_STARTTLS_ENABLE: "true"      # Enable STARTTLS (port 587)
# SPRING_MAIL_PROPERTIES_MAIL_SMTP_SSL_ENABLE: "true"  # Use instead of STARTTLS for port 465
# APP_MAIL_FROM: [email protected]          # Sender address (defaults to SMTP username)

SendGrid:

MAIL_PROVIDER: sendgrid
SENDGRID_API_KEY: SG.xxxxx                    # Your SendGrid API key
SENDGRID_FROM: [email protected]            # Sender address

Mailchimp / Mandrill:

MAIL_PROVIDER: mailchimp
MAILCHIMP_API_KEY: md-xxxxx                   # Mandrill API key

All email settings can also be configured via Godmode → Platform → Email.

Google OAuth

"Sign in with Google" on the login page; needs a Google Cloud Console OAuth 2.0 client.

GOOGLE_AUTH_ENABLED: "true"                              # Must be set to enable the login button
GOOGLE_CLIENT_ID: 123...apps.googleusercontent.com      # OAuth client ID
GOOGLE_CLIENT_SECRET: GOCSPX-...                        # OAuth client secret
GOOGLE_REDIRECT_URI: https://example.com/auth/google/callback  # Must match Google Console

Can also be configured via Godmode → Authentication → Google.

AI Assistant (OpenAI)

AI features (text generation, summaries) via the OpenAI API.

AI_ENABLED: "true"                            # Enable AI features
AI_PROVIDER: openai                           # openai (default) | ollama | custom
OPENAI_API_KEY: sk-proj-xxxxx                 # Your API key
OPENAI_MODEL: gpt-4o-mini                     # Model name (default: gpt-4o-mini)

All AI settings can also be configured at runtime via Godmode → Integrations → AI.

AI Assistant (Ollama — local/self-hosted)

A local LLM instead of OpenAI — no API key, models run on your server.

Step 1. Add Ollama to your docker-compose.yml:

ollama:
  image: ollama/ollama:latest
  volumes:
    - ollama_data:/root/.ollama

Add ollama_data: to your volumes: section.

Step 2. Start and pull a model:

docker compose up -d
docker exec layers-ollama-1 ollama pull qwen2.5:0.5b

Other models: llama3.2 (3B), gemma2:2b (2B), mistral (7B). Full list at ollama.com/library.

Step 3. Configure in Godmode:

Go to Godmode → Integrations → AI and select:

  • AI ProviderOllama (local)
  • Ollama Server URLhttp://ollama:11434
  • Model → pick from dropdown (loads from your Ollama server)
  • Enable toggle and save

Or via environment variables:

AI_ENABLED: "true"
AI_PROVIDER: ollama
OLLAMA_URL: http://ollama:11434               # Default if using Docker Compose service name
OPENAI_MODEL: qwen2.5:0.5b                    # Model to use (must be pulled first)

Note: Switching from LOCAL to S3 storage or from OpenAI to Ollama requires a container restart. Audio transcription (Whisper) is not supported with Ollama — only available with OpenAI.

Yandex OAuth

"Sign in with Yandex"; create an app at oauth.yandex.ru/client/new. Add scopes login:email and login:info.

YANDEX_AUTH_ENABLED: "true"
YANDEX_CLIENT_ID: your-client-id
YANDEX_CLIENT_SECRET: your-client-secret
YANDEX_REDIRECT_URI: https://example.com/auth/yandex/callback

Can also be configured via Godmode → Authentication → Yandex.

VK ID OAuth

Adds "Sign in with VK ID" button. Create an app in the VK ID console.

VK_AUTH_ENABLED: "true"
VK_CLIENT_ID: "51234567"
VK_CLIENT_SECRET: your-secure-key
VK_REDIRECT_URI: https://example.com/auth/vk/callback

Can also be configured via Godmode → Authentication → VK ID.

ESIA / Госуслуги (Russia only)

Adds "Sign in with Gosuslugi" button. Requires registration with the Ministry of Digital Development and a GOST/RSA certificate (p12/pfx format).

ESIA_AUTH_ENABLED: "true"
ESIA_CLIENT_ID: YOUR_SYSTEM_MNEMONIC
ESIA_REDIRECT_URI: https://example.com/auth/esia/callback
ESIA_CERT_PATH: /path/to/certificate.p12
ESIA_CERT_PASSWORD: your-cert-password

Can also be configured via Godmode → Authentication → ESIA.

Note: ESIA integration requires a registered information system and a qualified electronic signature certificate. This is not available for all organizations.

Stock Photos (Unsplash)

Image search when inserting images; the key stays on the backend.

UNSPLASH_ENABLED: "true"                      # Must be set to enable image search
UNSPLASH_ACCESS_KEY: your-key                 # Get at unsplash.com/developers
GIF Search (Giphy)

GIF search; same server-side proxy as Unsplash.

GIPHY_ENABLED: "true"                         # Must be set to enable GIF search
GIPHY_API_KEY: your-key                       # Get at developers.giphy.com
S3 File Storage

By default, uploads are stored locally in the Docker volume. Switch to S3 for cloud storage:

FILE_STORAGE_TYPE: S3                         # Switch from LOCAL to S3
USE_AWS: "true"                               # Enable AWS SDK
S3_BUCKET_NAME: my-bucket                     # S3 bucket name
S3_REGION: eu-central-1                       # AWS region
S3_ACCESS_KEY: AKIA...                        # AWS access key
S3_SECRET_KEY: your-secret                    # AWS secret key

Works with any S3-compatible storage (MinIO, DigitalOcean Spaces, etc.)

Note: Switching from LOCAL to S3 requires a container restart to initialize the S3 connection. Switching from S3 to LOCAL takes effect immediately.

S3 can also be configured via Godmode → System → Storage.

Upload Size Limits

The default max upload size is 900 MB. To change it:

SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: "2GB"
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: "2GB"

Can also be configured via Godmode → System → Storage → Max File Size.

If using Nginx, also update client_max_body_size in your Nginx config to match or exceed this value.

Elasticsearch

Full-text search; needs a running Elasticsearch.

SEARCH_ELASTICSEARCH_ENABLED: "true"
ES_HOST: elasticsearch                        # Elasticsearch host
ES_PORT: "9200"                               # Elasticsearch port
ES_USERNAME: elastic                          # Auth username (if enabled)
ES_PASSWORD: changeme                         # Auth password (if enabled)
Sentry (Error Monitoring)

Backend error tracking with Sentry.

USE_SENTRY: "true"                                    # Required to enable error reporting
SENTRY_DSN: https://[email protected]/456    # Sentry DSN
SENTRY_ENVIRONMENT: production                        # Environment tag
SENTRY_TRACES_SAMPLE_RATE: "0.1"                     # % of requests traced (default: 0.0 = disabled)

Important: USE_SENTRY: "true" must be set as an environment variable — configuring DSN in Godmode alone is not sufficient. The Sentry error transport is initialized at startup and requires this flag.

Frontend Analytics
FRONTEND_ANALYTICS_YANDEX_METRIKA_ID: "12345"     # Yandex.Metrika counter
FRONTEND_ANALYTICS_GTM_ID: GTM-XXXXXXX            # Google Tag Manager
Branding

Customize the app name, logo, and social links:

FRONTEND_APP_NAME: "My Company"                       # App name shown in UI
FRONTEND_APP_URL: https://app.company.com             # App URL for emails
FRONTEND_LOGO_FULL_URL: https://cdn.company.com/logo.png   # Full logo URL
FRONTEND_LOGO_ICON_URL: https://cdn.company.com/icon.png   # Icon/favicon URL

Social links (shown in footer/about):

FRONTEND_SOCIAL_FACEBOOK: https://facebook.com/mypage
FRONTEND_SOCIAL_INSTAGRAM: https://instagram.com/mypage
FRONTEND_SOCIAL_LINKEDIN: https://linkedin.com/company/myco
FRONTEND_SOCIAL_TELEGRAM: https://t.me/mychannel
FRONTEND_SOCIAL_DISCORD: https://discord.gg/myinvite
FRONTEND_SOCIAL_WHATSAPP: https://wa.me/123456789
External Database

Replace the db service with your own PostgreSQL:

DB_SERVER_URL: jdbc:postgresql://your-host:5432/your_db  # Your database JDBC URL
DB_SERVER_USERNAME: your_user                             # Database user
DB_SERVER_PASSWORD: your_password                         # Database password

Remove the db service and pg_data volume from docker-compose.yml.

MCP Server (AI Agent Integration)

Lets Claude Code, Cursor and other AI tools use workspace data via Model Context Protocol.

# Add as a separate service:
layers-mcp:
  image: sinups/layers-mcp-server:latest
  depends_on:
    - layers
  environment:
    LAYERS_BACKEND_BASEURL: http://layers:8080
    LAYERS_BACKEND_FORWARDINCOMINGAUTH: "true"
    LAYERS_BACKEND_REQUIREINCOMINGAUTH: "true"
    SERVER_PORT: "8091"

Users create personal API tokens in Settings > Access Tokens.

Seller / Distribution (partners only)

For resellers: enables the Distribution section in Godmode with download pages.

LAYERS_SELLER: "true"                                    # Enable Distribution in Godmode
LAYERS_REGION: "EU"                                      # Region code (EU, RU, US) — affects social links
LAYERS_DOWNLOAD_DESKTOP_ENABLED: "true"                  # Show /download page
LAYERS_DOWNLOAD_DESKTOP_MAC_URL: https://cdn.example.com/Layers.dmg
LAYERS_DOWNLOAD_DESKTOP_WIN_URL: https://cdn.example.com/Layers-Setup.exe
LAYERS_DOWNLOAD_DESKTOP_LINUX_URL: https://cdn.example.com/Layers.AppImage
LAYERS_DOWNLOAD_SELFHOST_ENABLED: "true"                 # Show /download-install page
LAYERS_DOWNLOAD_SELFHOST_TAR_GZ_URL: /files/layers-server-latest.tar.gz
LAYERS_DOWNLOAD_SELFHOST_TAR_URL: /files/layers-server-latest.tar

Troubleshooting

"password authentication failed"

PostgreSQL sets the password only when the volume is created. If you changed POSTGRES_PASSWORD later:

Option A — reset database (loses data):

docker compose down -v
docker compose up -d

Option B — change password without losing data:

docker exec -it layers-db-1 psql -U postgres -c "ALTER USER layers PASSWORD 'your_new_password'"
Application won't start

Check logs:

docker compose logs layers
CORS errors in browser

Your APP_CORS_ALLOWED_ORIGINS doesn't match the URL in your browser. The value must match exactly (including https:// and port if any).

Cookie conflicts when using a custom domain alongside app.layers.md

A subdomain of layers.md next to app.layers.md gets conflicting cookies: set LAYERS_DOMAIN and LAYERS_APP_DOMAIN to your domain and restart.

"workspaces_plan_check" constraint error on startup

If the server fails to start with violates check constraint "workspaces_plan_check":

docker exec -it layers-db-1 psql -U layers -d layersdb \
  -c "ALTER TABLE workspaces DROP CONSTRAINT IF EXISTS workspaces_plan_check;"
docker compose restart layers
502/503 Bad Gateway after update

The app needs ~15–60 s to start; the healthcheck restarts it if unresponsive.

docker compose logs layers --tail=20

If you see Started LayersApplication, the server is ready.

File uploads fail with "ERR_NOT_MULTIPART" or 413

ERR_NOT_MULTIPART: the upload was truncated — Nginx client_max_body_size is smaller than the file. Add:

client_max_body_size 1000m;

413: Nginx or Spring rejects the file. Check both:

  • Nginx: client_max_body_size
  • Spring: SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE env var (default 900MB)
JWT token invalid after container restart

The JWT secret lives in the layers_data volume; lose the volume and everyone must log in again.


Image Tags

TagDescription
latestLatest stable release (recommended for all installs); from 2.1.184-unified-20260920-cve: license page, 90-day node mode, Docker Scout 0 critical / 0 high
{VERSION}Pinned semver release (e.g. 1.0.0)

Tag summary

Content type

Image

Digest

sha256:9dda9dd98

Size

666.5 MB

Last updated

3 days ago

docker pull hissih/layers