Author:
    Creation:2026-06-30Last update:2026-09-27

    Self-Hosting Intlayer

    Intlayer can run on your own infrastructure, no Intlayer Cloud account required. Three setups are available, all driven by the same installer (install.sh, install.ps1 on Windows, or npx intlayer init infra):

    SetupWhat it isPick it for
    Desktop appNative dashboard for macOS, Linux and WindowsA local client, nothing to host
    All-in-one DockerDashboard, API, MongoDB, Redis and MinIO in a single containerTrials and small single-box installs
    Docker ComposeOne container per service, each datastore replaceable by a managed offeringProduction, scaling, managed datastores

    Table of Contents

    Published images and packages

    ArtifactDocker HubGHCR mirrorContents
    All-in-one containerintlayer/cms-allghcr.io/aymericzip/intlayer/cms-allapp + backend + MongoDB 8 + Redis + MinIO + Chromium
    Dashboard (frontend)intlayer/cms-frontendghcr.io/aymericzip/intlayer/cms-frontendTanStack Start dashboard on Bun
    API (backend)intlayer/cms-backendghcr.io/aymericzip/intlayer/cms-backendFastify REST API on Bun + Chromium
    Desktop appGitHub releasesn/a.dmg (macOS), .deb / .rpm / .AppImage (Linux), .exe / .msi (Windows)

    All three images are built from the same docker/selfhost/Dockerfile and published on every release. The Compose stack also pulls the official mongo:8, redis:8-alpine and quay.io/minio/minio images.

    Setup

    The installer asks which setup you want, checks the prerequisites (offering to install Docker), writes the environment file with the secrets already generated, and pulls the images. It never starts anything on its own: the Docker modes need a mailer first, so it ends by printing the command to run. Re-running it is safe: an existing environment file is never overwritten, which makes it the upgrade path too.

    The Intlayer dashboard as a native application, built with Tauri. It signs in to the Intlayer Cloud (https://app.intlayer.org), so there is nothing to host. It is the right choice when you want a local client rather than a browser tab.

    Install

    The installer downloads the package for your OS and CPU, then opens it (macOS), installs it (dpkg / rpm on Linux) or launches the setup wizard (Windows). You can also download it by hand from the releases page.

    sh
    curl -fsSL https://intlayer.org/install.sh | sh -s -- --mode desktop
    

    In PowerShell:

    powershell
    $env:INTLAYER_MODE = "desktop"; irm https://intlayer.org/install.ps1 | iex
    
    bash
    npx intlayer init infra --mode desktop
    

    Requirements

    • Node.js: the app embeds the dashboard's server and starts it with the machine's own node binary. Install it from nodejs.org if the app does not start.
    The published desktop build talks to the Intlayer Cloud backend. Pointing it at a self-hosted backend requires rebuilding the app with VITE_BACKEND_URL set to your API, see Limitations.

    Everything runs inside the single intlayer/cms-all container, supervised by s6-overlay, with every datastore persisted under one volume.

    plaintext
                    ┌─────────────────────────────┐
     browser ──────▶ │  app  (TanStack Start)  :3000│ ──┐
     (localhost)    └─────────────────────────────┘   │ VITE_BACKEND_URL (baked at build)
                    ┌─────────────────────────────┐   │
                    │  backend (Fastify/Bun)  :3100│ ◀─┘
                    └──────────────┬──────────────┘
              ┌──────────┬─────────┼──────────────┐
              ▼          ▼         ▼               ▼
          mongo:27017  redis:6379  minio:9000   Chromium
          /data/mongo  /data/redis /data/minio  (in-image)
          (1-node RS)              minio:9001
    
    ServiceHost port(s)Purpose
    app3000Dashboard (CMS UI)
    backend3100REST API (/health endpoint)
    mongointernalMongoDB 8, single-node replica set rs0
    redisinternalJob queues (BullMQ) and caching
    minio9000 (S3), 9001 (console)S3-compatible object storage for avatars and screenshots

    Boot order is enforced by s6 dependencies (mongod → replica-set init, minio → bucket creation, then backend, then app), and services restart on exit, so the first boot recovers on its own.

    Prerequisites

    • Docker ≥ 24: the installer offers to install it (via get.docker.com on Linux, Homebrew on macOS). On Windows, install Docker Desktop (WSL 2 backend) first.
    • Ports 3000, 3100, 9000 and 9001 free on the host. MinIO 9000 must stay reachable by the browser, which loads assets straight from S3_PUBLIC_URL.
    • A mailer: a Resend API key or an SMTP relay.

    1. Install

    Writes ./intlayer.env with BETTER_AUTH_SECRET and S3_SECRET_ACCESS_KEY generated, asks a few questions to fill in the rest, and pulls intlayer/cms-all:latest.

    sh
    curl -fsSL https://intlayer.org/install.sh | sh -s -- --mode docker
    

    In PowerShell:

    powershell
    $env:INTLAYER_MODE = "docker"; irm https://intlayer.org/install.ps1 | iex
    
    bash
    npx intlayer init infra --mode docker
    

    2. Answer the setup questions

    The installer asks for (press Enter to accept a suggestion, every answer can be changed in the file later):

    • The domain Intlayer is served on. Leave it empty to stay on localhost. With a domain such as example.org, it suggests https://cms.example.org for the dashboard, https://back.example.org for the API and https://s3.example.org/intlayer for the object storage, and writes DOMAIN, APP_URL, BACKEND_URL and S3_PUBLIC_URL. See Custom domain for what follows.
    • The mailer: Resend (API key) or an SMTP relay (host, port, credentials), plus the sender address. This can be skipped and done by hand later.
    • An optional OpenAI API key for the AI features.

    Without a terminal (for instance when the script is run from CI), the questions are skipped and only the secrets are generated. Open intlayer.env and fill in Resend or SMTP by hand (details in Global mailer):

    intlayer.env
    # Option A: Resend
    RESEND_API_KEY=<your-resend-key>
    
    # Option B: SMTP (takes over from Resend as soon as MAIL_SMTP_HOST is set)
    MAIL_SMTP_HOST=smtp.example.com
    MAIL_SMTP_PORT=587
    MAIL_SMTP_USER=<user>
    MAIL_SMTP_PASSWORD=<password>
    MAIL_FROM=Intlayer <no-reply@example.com>
    

    3. Start

    This is the command the installer prints (with a custom domain, it is preceded by the docker build that produces intlayer/cms-all:custom, see Custom domain):

    sh
    docker run -d --name intlayer \
      --restart unless-stopped \
      -p 3000:3000 -p 3100:3100 -p 9000:9000 -p 9001:9001 \
      -v intlayer-data:/data \
      --env-file ./intlayer.env \
      intlayer/cms-all:latest
    
    powershell
    docker run -d --name intlayer `
      --restart unless-stopped `
      -p 3000:3000 -p 3100:3100 -p 9000:9000 -p 9001:9001 `
      -v intlayer-data:/data `
      --env-file ./intlayer.env `
      intlayer/cms-all:latest
    

    The CLI runs the installer, which prints the docker run … command shown in the other tabs. Copy it into your terminal once the mailer is configured.

    Open http://localhost:3000 (or your dashboard URL) and follow First-run setup. The first boot initialises the replica set and the bucket, so give it a minute.

    Backup and upgrade

    All state lives in the intlayer-data volume (/data/mongo, /data/redis, /data/minio).

    sh
    # Backup (stop the container first so MongoDB's files are consistent)
    docker stop intlayer
    docker run --rm -v intlayer-data:/data -v "$(pwd)":/backup busybox tar czf /backup/intlayer-data.tar.gz /data
    docker start intlayer
    
    # Restore
    docker run --rm -v intlayer-data:/data -v "$(pwd)":/backup busybox tar xzf /backup/intlayer-data.tar.gz -C /
    

    To upgrade, re-run the installer (it pulls the latest image and keeps intlayer.env), then docker rm -f intlayer and run the start command again. To use a managed MongoDB instead of the bundled one, set MONGODB_URI in intlayer.env.

    One container per service on a private Compose network. The dashboard and the API use the published intlayer/cms-frontend and intlayer/cms-backend images; the datastores use the official mongo, redis and minio images.

    plaintext
                    ┌───────────────────┐
     browser ──────▶ │  app        :3000 │ ── SSR ──▶ http://backend:3100
     (localhost)    └───────────────────┘
                    ┌───────────────────┐
     browser ──────▶ │  backend    :3100 │
     (localhost)    └─────────┬─────────┘
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
         mongo:27017     redis:6379      minio:9000 ◀── browser (assets)
         (1-node RS)                     minio:9001
    
    ServiceImageRole
    appintlayer/cms-frontendDashboard on :3000; waits for the backend to be healthy
    backendintlayer/cms-backendAPI on :3100 with Chromium; waits for Mongo, Redis and the MinIO bucket
    mongomongo:8Single-node replica set rs0, initiated by its own healthcheck
    redisredis:8-alpineQueues and caching, append-only persistence
    minioquay.io/minio/minioS3 storage on :9000, console on :9001
    minio-initquay.io/minio/mcOne-shot: creates the bucket and its anonymous-download policy

    Data is kept in the intlayer_mongo-data, intlayer_redis-data and intlayer_minio-data volumes. The service wiring (MONGODB_URI, REDIS_URL, S3_ENDPOINT, the internal backend URL used by server-side rendering) is fixed in the compose file and takes precedence over .env, which only carries secrets and optional integrations.

    Prerequisites

    • Docker ≥ 24 with the Compose plugin: the installer offers to install it on Linux and macOS. On Windows, install Docker Desktop (WSL 2 backend) first.
    • Ports 3000, 3100, 9000 and 9001 free on the host.
    • A mailer: a Resend API key or an SMTP relay.

    1. Install

    Writes docker-compose.yml and a .env with the secrets generated into ./intlayer/, asks the same setup questions as the all-in-one mode (domain, mailer, OpenAI key), and pulls the images.

    sh
    curl -fsSL https://intlayer.org/install.sh | sh -s -- --mode compose
    

    Or by hand:

    sh
    mkdir intlayer && cd intlayer
    curl -fsSLO https://raw.githubusercontent.com/aymericzip/intlayer/main/docker/selfhost/docker-compose.yml
    curl -fsSL  https://raw.githubusercontent.com/aymericzip/intlayer/main/docker/selfhost/.env.template -o .env
    # fill in BETTER_AUTH_SECRET and S3_SECRET_ACCESS_KEY (openssl rand -hex 32)
    

    In PowerShell:

    powershell
    $env:INTLAYER_MODE = "compose"; irm https://intlayer.org/install.ps1 | iex
    

    Or by hand:

    powershell
    mkdir intlayer; cd intlayer
    irm https://raw.githubusercontent.com/aymericzip/intlayer/main/docker/selfhost/docker-compose.yml -OutFile docker-compose.yml
    irm https://raw.githubusercontent.com/aymericzip/intlayer/main/docker/selfhost/.env.template -OutFile .env
    # fill in BETTER_AUTH_SECRET and S3_SECRET_ACCESS_KEY
    
    bash
    npx intlayer init infra --mode compose
    

    2. Configure a mailer

    If you skipped the mailer question, fill in Resend or SMTP in intlayer/.env, exactly as for the all-in-one container (see Global mailer).

    3. Start

    sh
    cd intlayer && docker compose up -d
    

    With a custom domain, the installer also downloads docker-compose.build.yml and the start command becomes docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build (see Custom domain).

    Open http://localhost:3000 (or your dashboard URL) and follow First-run setup.

    Managed datastores

    Delete the service you are replacing from the compose file (and its depends_on entry on backend), then override the matching variable:

    docker-compose.yml
    services:
      backend:
        environment:
          MONGODB_URI: mongodb+srv://user:password@cluster0.xxxxx.mongodb.net/intlayer
          REDIS_URL: rediss://default:password@redis.example.com:6380
          S3_ENDPOINT: https://s3.eu-west-1.amazonaws.com
          S3_PUBLIC_URL: https://intlayer-assets.s3.eu-west-1.amazonaws.com
    

    S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY / S3_BUCKET_NAME keep their meaning against any S3-compatible provider.

    Scaling

    app and backend are stateless. Behind a load balancer, docker compose up -d --scale backend=3 works once the fixed host port mappings are removed and the proxy addresses the services by name. Background jobs are coordinated through Redis (BullMQ), so several backend replicas share the queue safely.

    Building from source

    An override switches the two Intlayer services from image: to build:. From a checkout of the repository:

    sh
    cd docker/selfhost
    docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
    

    Without a checkout, point the build context at the repository itself by setting INTLAYER_BUILD_CONTEXT=https://github.com/aymericzip/intlayer.git#main in .env. The dashboard's VITE_* build args follow DOMAIN, APP_URL and BACKEND_URL from the same file, which is how a custom domain is applied.

    Backup and upgrade

    sh
    # Backup one volume (repeat for intlayer_redis-data and intlayer_minio-data)
    docker compose stop
    docker run --rm -v intlayer_mongo-data:/data -v "$(pwd)":/backup busybox tar czf /backup/mongo-data.tar.gz /data
    docker compose start
    
    # Upgrade, volumes are kept
    docker compose pull && docker compose up -d
    

    Installer settings

    Without --mode (or INTLAYER_MODE), the installer shows a menu: desktop, docker (all-in-one) or compose. It also reads a few environment variables. Because it is piped into the shell, pass them to the shell rather than to curl:

    sh
    curl -fsSL https://intlayer.org/install.sh | INTLAYER_COMPOSE_DIR=./cms sh -s -- --mode compose
    
    powershell
    $env:INTLAYER_MODE = "compose"; $env:INTLAYER_COMPOSE_DIR = ".\cms"; irm https://intlayer.org/install.ps1 | iex
    
    VariableDefaultApplies toDescription
    INTLAYER_MODE(asked)alldesktop, docker or compose, same as --mode
    INTLAYER_DOWNLOAD_DIR~/DownloadsdesktopWhere the app installer is saved
    INTLAYER_IMAGEintlayer/cms-all:latestdockerAll-in-one image to pull
    INTLAYER_ENV_FILE./intlayer.envdockerWhere to write the environment file
    INTLAYER_CONTAINER_NAMEintlayerdockerContainer name
    INTLAYER_DATA_VOLUMEintlayer-datadockerNamed volume mounted at /data
    INTLAYER_APP_PORT3000dockerHost port for the dashboard
    INTLAYER_API_PORT3100dockerHost port for the API
    INTLAYER_S3_PORT9000dockerHost port for the MinIO S3 API
    INTLAYER_CONSOLE_PORT9001dockerHost port for the MinIO console
    INTLAYER_COMPOSE_DIR./intlayercomposeWhere docker-compose.yml and .env are written
    INTLAYER_SELFHOST_REFmainbothGit ref the compose file and env template are fetched from
    INTLAYER_BUILD_CONTEXT…/intlayer.git#mainbothBuild context used when a custom domain requires a rebuild
    INTLAYER_CUSTOM_IMAGEintlayer/cms-all:customdockerTag of the all-in-one image built for a custom domain
    The port variables only change the host side of the mapping. The published images have http://localhost:3000, http://localhost:3100 and http://localhost:9000 compiled into the dashboard bundle, so remapping them leaves the browser pointing at the old ports. Keep the defaults unless you build your own images, see Limitations.

    First-run setup

    On a fresh instance (empty database), opening the dashboard redirects you to the /init page:

    1. Create the first account. Because the users collection is empty, this account is automatically promoted to super admin.
    2. A verification email is sent through Resend or your SMTP relay. Email verification is mandatory, this is why a mailer must be configured before you start.
    3. Click the link in the email, then sign in.

    Once an admin exists, /init redirects to the standard sign-in page.

    Environment variables

    Both Docker modes read the same file (intlayer.env for the container, .env for Compose), generated from docker/selfhost/.env.template.

    Required

    VariableExampleDescription
    BETTER_AUTH_SECRET(generated)32-byte secret for session signing
    S3_SECRET_ACCESS_KEY(generated)Secret for the bundled MinIO
    RESEND_API_KEY(your key)Transactional email via Resend. Required for first-run setup unless an SMTP relay is configured instead (see Global mailer)

    Fixed by the deployment

    These are set by the image (all-in-one) or by the compose file, and only need overriding for a non-standard topology. DOMAIN, APP_URL, BACKEND_URL and S3_PUBLIC_URL are the exception: set in the env file, they take precedence in both modes (see Custom domain).

    VariableAll-in-oneDocker ComposeDescription
    PORT31003100Backend listening port
    APP_URLhttp://localhost:3000http://localhost:3000Public URL of the dashboard
    BACKEND_URLhttp://localhost:3100http://localhost:3100Public URL of the backend API
    DOMAINlocalhostlocalhostCookie domain
    SELF_HOSTEDtruetrueDisables the cloud-only API endpoints (billing, subscriptions, marketplace)
    MONGODB_URImongodb://127.0.0.1:27017/intlayer?replicaSet=rs0mongodb://mongo:27017/…MongoDB connection string, any mongodb:// or mongodb+srv:// cluster works
    REDIS_URLredis://127.0.0.1:6379redis://redis:6379Redis
    S3_ENDPOINThttp://127.0.0.1:9000http://minio:9000MinIO (server-to-server)
    S3_PUBLIC_URLhttp://localhost:9000/intlayerhttp://localhost:9000/intlayerPublic URL for browser asset loading
    S3_BUCKET_NAMEintlayerintlayerBucket name
    S3_ACCESS_KEY_IDintlayerintlayerMinIO access key

    The Compose app service additionally receives INTLAYER_BACKEND_INTERNAL_URL=http://backend:3100: the browser reaches the API on localhost:3100, but server-side rendering runs inside the Compose network and must use the service name.

    Custom domain

    The backend reads its public URLs at runtime, but the dashboard has them compiled in: the published intlayer/cms-frontend and intlayer/cms-all images only work on http://localhost:3000. Serving Intlayer on your own domain therefore takes two things, both prepared by the installer when you answer the domain question:

    1. Four variables in the env file, read by the backend (cookies, email links, OAuth callbacks, asset URLs) and used as build args by docker-compose.build.yml:

      intlayer.env
      DOMAIN=example.org                          # cookie domain, parent of the hosts below
      APP_URL=https://cms.example.org
      BACKEND_URL=https://back.example.org
      S3_PUBLIC_URL=https://s3.example.org/intlayer
      
    2. A dashboard image built with those URLs. Docker builds it straight from the repository, no checkout needed:

      sh
      # Docker Compose: the override reads the build args from .env
      docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
      
      # All-in-one
      docker build -f docker/selfhost/Dockerfile \
        --build-arg VITE_DOMAIN=example.org \
        --build-arg VITE_SITE_URL=https://cms.example.org \
        --build-arg VITE_IDE_URL=https://cms.example.org \
        --build-arg VITE_BACKEND_URL=https://back.example.org \
        -t intlayer/cms-all:custom \
        https://github.com/aymericzip/intlayer.git#main
      

    Then put a reverse proxy with TLS in front of the container: cms.example.org → port 3000, back.example.org → 3100, s3.example.org → 9000. The three hosts must share the DOMAIN suffix, since the session cookie is scoped to it.

    Optional (features degrade gracefully when absent)

    VariableFeature
    OPENAI_API_KEYAI-assisted translation and content audit
    GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRETGitHub OAuth login
    GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRETGoogle OAuth login
    GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRETGitLab OAuth login
    MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRETMicrosoft OAuth login

    Global mailer

    Every transactional email, including non-organization emails such as password resets and magic links, goes through one of two global transports:

    • Resend, using RESEND_API_KEY.
    • SMTP, using the MAIL_SMTP_* variables. As soon as MAIL_SMTP_HOST is set, SMTP is used and RESEND_API_KEY is ignored.

    MAIL_PROVIDER is only needed to force one transport when both are configured (for instance MAIL_PROVIDER=resend to keep Resend while an SMTP host is present).

    VariableExampleDescription
    MAIL_FROMIntlayer <no-reply@acme.com>Sender header for either transport. Accepts a bare address or Name <email>
    MAIL_SMTP_HOSTsmtp.acme.comSMTP host. Setting it selects the SMTP transport
    MAIL_SMTP_PORT587SMTP port (defaults to 587)
    MAIL_SMTP_SECUREfalseImplicit TLS. Set true for port 465
    MAIL_SMTP_USER(your user)SMTP username (optional; omit for unauthenticated relays)
    MAIL_SMTP_PASSWORD(your password)SMTP password
    MAIL_PROVIDERresendOptional override: smtp or resend. Leave unset to auto-select
    Precedence: an organization's own mailer (configured from the Organization dashboard) takes priority over the global mailer, which in turn takes priority over the default Resend key.

    Connecting your Intlayer project

    Once the stack is running, point your project at the self-hosted backend and dashboard instead of intlayer.org.

    Project configuration

    intlayer.config.ts
    import type { IntlayerConfig } from "intlayer";
    
    const config: IntlayerConfig = {
      editor: {
        clientId: process.env.INTLAYER_CLIENT_ID,
        clientSecret: process.env.INTLAYER_CLIENT_SECRET,
    
        /**
         * URL of the self-hosted CMS dashboard.
         * Default: https://app.intlayer.org
         */
        cmsURL: process.env.INTLAYER_CMS_URL, // e.g. http://localhost:3000
    
        /**
         * URL of the self-hosted backend API.
         * Default: https://back.intlayer.org
         */
        backendURL: process.env.INTLAYER_BACKEND_URL, // e.g. http://localhost:3100
      },
    };
    
    export default config;
    

    Set the environment variables in your project's .env:

    sh
    INTLAYER_CMS_URL=http://localhost:3000
    INTLAYER_BACKEND_URL=http://localhost:3100
    INTLAYER_CLIENT_ID=<your-client-id>
    INTLAYER_CLIENT_SECRET=<your-client-secret>
    

    Create access credentials in your self-hosted dashboard under Projects → Access keys at http://localhost:3000/projects.

    @intlayer/api SDK

    When using the @intlayer/api SDK programmatically, pass backendURL explicitly:

    cms.ts
    import { createIntlayerCMS } from "@intlayer/api";
    import { dictionaryEndpoint } from "@intlayer/api/dictionary";
    
    const cms = createIntlayerCMS({
      editor: {
        clientId: process.env.INTLAYER_CLIENT_ID,
        clientSecret: process.env.INTLAYER_CLIENT_SECRET,
        backendURL: process.env.INTLAYER_BACKEND_URL, // http://localhost:3100
      },
    });
    
    const { data: dictionaries } = await dictionaryEndpoint(cms).getDictionaries();
    

    Limitations

    • A custom domain means a rebuild. All browser-facing VITE_* URLs are inlined into the dashboard at build time, and the published images (and the desktop app) ship with localhost / Intlayer Cloud values. Out of the box the dashboard must be accessed at http://localhost:3000, the API at :3100 and MinIO at :9000; remapping the host ports has the same effect. The installer wires everything for a rebuild from the repository when you give it a domain (see Custom domain), but the build itself takes several minutes. Pointing the desktop app at a self-hosted backend is not supported.
    • Email requires a working mailer. First-run setup enforces email verification, so either RESEND_API_KEY or an SMTP relay (MAIL_SMTP_*) must be configured. After the first admin signs in, each organization can also configure its own SMTP or Resend mailer from the dashboard.
    • The desktop app needs Node.js on the machine to start its embedded server.
    • No documentation assistant. The AI doc assistant of intlayer.org (/api/ai/ask, /api/search/doc) relies on ~130 MB of pre-computed documentation embeddings that the self-host images do not ship; those two routes are not registered in self-hosted mode. The dashboard's own AI features (translation, audit, autocomplete, chat) are unaffected and only need OPENAI_API_KEY.