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

    Self-Hosting Intlayer

    Intlayer can run entirely on your own infrastructure. No Intlayer Cloud account required. A single command boots a production-ready stack:

    One command installs everything:

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

    The installer downloads a docker-compose.yml and a .env, auto-generates the required secrets, and starts all containers with docker compose up -d.

    The only external dependency is MongoDB: the backend connects to a MongoDB Atlas cluster, which you provide. Everything else runs inside the container.

    Table of Contents


    Architecture

    plaintext
                    ┌─────────────────────────────┐
     browser ──────▶ │  app  (TanStack Start)  :3000│ ──┐
                    └─────────────────────────────┘   │ VITE_BACKEND_URL
                    ┌─────────────────────────────┐   │
                    │  backend (Fastify/Bun)  :3100│ ◀─┘
                    └──────────────┬──────────────┘
              ┌──────────┬─────────┼──────────┬───────────┐
              ▼          ▼         ▼          ▼           ▼
         mongo:27017  redis:6379  minio:9000  mailpit:1025  Chromium
         (1-node RS)             (S3 API)     (SMTP)        (in-image)
                                 minio:9001   mailpit:8025
                                 (console)    (web UI)
    

    Chromium (used for Puppeteer screenshot generation) is bundled inside the backend image. No separate container is needed.


    Prerequisites

    • Docker ≥ 24 and Docker Compose ≥ v2. If either is missing, the installer prints the install link and exits.
    • Ports 3000, 3100, 8025, 9000, and 9001 available on the host.
    • A Linux or macOS host (or WSL2 on Windows).

    Everything else — Bun, Redis, MinIO, Chromium — ships inside the image.


    Quick start

    1. Run the installer

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

    It verifies Docker is installed and running, writes ./intlayer.env with BETTER_AUTH_SECRET and S3_SECRET_ACCESS_KEY already generated, and pulls the image. It does not start the container — the backend cannot boot without your database credentials.

    Re-running the installer is safe: an existing intlayer.env is never overwritten, so it doubles as the upgrade path.

    2. Fill in your credentials

    Open intlayer.env and complete the values marked TODO:

    intlayer.env
    DB_ID=<atlas-user>
    DB_MDP=<atlas-password>
    DB_CLUSTER=<cluster>.xxxxx.mongodb.net
    RESEND_API_KEY=<your-resend-key>
    

    The file also carries commented-out blocks for the optional features — SMTP mailer, OPENAI_API_KEY, and the OAuth providers. Uncomment what you need.

    The file is read by docker run --env-file, which does not strip quotes and treats everything after = as the value. Write bare values, and keep comments on their own lines.

    3. Start the container

    This is the command the installer prints when it finishes:

    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 \
      ghcr.io/aymericzip/intlayer-selfhost:latest
    

    Then open http://localhost:3000. First boot initialises the datastores, so give it a minute.

    The dashboard is served on localhost. See Limitations — custom domains are not supported by the published image.

    Installer settings

    The installer reads a few environment variables. Because it is piped into sh, pass them to the shell rather than to curl:

    sh
    curl -fsSL https://intlayer.org/install.sh | INTLAYER_ENV_FILE=./config/intlayer.env sh
    
    Variable Default Description
    INTLAYER_IMAGE ghcr.io/aymericzip/intlayer-selfhost:latest Image to pull
    INTLAYER_ENV_FILE ./intlayer.env Where to write the env file
    INTLAYER_CONTAINER_NAME intlayer Container name
    INTLAYER_DATA_VOLUME intlayer-data Named volume mounted at /data
    INTLAYER_APP_PORT 3000 Host port for the dashboard
    INTLAYER_API_PORT 3100 Host port for the API
    INTLAYER_S3_PORT 9000 Host port for the MinIO S3 API
    INTLAYER_CONSOLE_PORT 9001 Host port for the MinIO console
    The four port variables only change the host side of the mapping printed in the docker run command. The published image has http://localhost:3000, http://localhost:3100 and http://localhost:9000 compiled into the dashboard bundle at build time, so remapping them leaves the browser pointing at the old ports. Keep the defaults unless you are building your own image — see Limitations.

    Quick start

    What the installer does:

    1. Checks that docker and docker compose are present.
    2. Downloads docker-compose.yml and .env.example into ./intlayer/.
    3. If no .env exists, copies the example and generates random secrets for BETTER_AUTH_SECRET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY via openssl rand.
    4. Runs docker compose pull + docker compose up -d.
    5. Prints the URLs: dashboard :3000, API :3100, email UI :8025, MinIO console :9001.

    After the stack is up, open http://localhost:3000 and create your first account.


    Services

    Service Image Host port(s) Purpose
    app built from apps/app/Dockerfile 3000 TanStack Start dashboard (CMS UI)
    backend built from apps/backend/Dockerfile 3100 Fastify REST API (/health endpoint)
    mongo mongo:7 internal Single-node replica set (rs0)
    redis redis:7-alpine internal Job queues (BullMQ) and caching (ioredis)
    minio minio/minio 9000 (S3), 9001 (console) S3-compatible object storage for avatars and screenshots
    mailpit axllent/mailpit 1025 (SMTP), 8025 (web UI) Local transactional email sink
    MinIO port 9000 must be reachable by the browser because uploaded assets (avatars, screenshots) are loaded directly from S3_PUBLIC_URL=http://localhost:9000/intlayer.

    Environment variables

    Required

    Variable Example Description
    DB_ID intlayer MongoDB Atlas user
    DB_MDP (your password) MongoDB Atlas password
    DB_CLUSTER cluster0.xxxxx.mongodb.net MongoDB Atlas cluster host (used in the mongodb+srv:// URI)
    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 you configure a global SMTP mailer (see Global mailer)

    Required (auto-generated or prompted)

    Variable Example Description
    NODE_ENV production Runtime environment
    PORT 3100 Backend listening port
    BACKEND_URL http://localhost:3100 Public URL of the backend API
    APP_URL http://localhost:3000 Public URL of the dashboard
    DOMAIN localhost Cookie domain
    MONGODB_URI mongodb://mongo:27017/intlayer?replicaSet=rs0 Full MongoDB connection URI
    REDIS_URL redis://redis:6379 Redis connection URL
    BETTER_AUTH_SECRET (generated) 32-byte secret for session signing
    MAIL_PROVIDER smtp Mail transport: smtp or resend
    MAIL_SMTP_HOST mailpit SMTP hostname (Mailpit container name)
    MAIL_SMTP_PORT 1025 SMTP port
    MAIL_FROM Intlayer <no-reply@localhost> Sender address
    S3_ENDPOINT http://minio:9000 S3-compatible endpoint
    S3_PUBLIC_URL http://localhost:9000/intlayer Public URL for browser asset loading
    S3_BUCKET_NAME intlayer Bucket name
    S3_ACCESS_KEY_ID (generated) MinIO access key
    S3_SECRET_ACCESS_KEY (generated) MinIO secret key
    VITE_BACKEND_URL http://localhost:3100 Backend URL baked into the dashboard at build time
    VITE_DOMAIN localhost Domain baked into the dashboard at build time

    Optional (features degrade gracefully when absent)

    Variable Feature
    OPENAI_API_KEY AI-assisted translation and content audit
    STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_* Billing and subscription management
    RESEND_API_KEY Transactional email via Resend (overrides Mailpit when set)
    GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET GitHub OAuth login
    GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET Google OAuth login
    GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET GitLab OAuth login
    MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET Microsoft OAuth login
    LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET LinkedIn OAuth login
    ATLASSIAN_CLIENT_ID, ATLASSIAN_CLIENT_SECRET Atlassian OAuth login

    Global mailer

    By default, all transactional emails are sent through Resend using RESEND_API_KEY. Self-hosted deployments can instead route every email — including non-organisation emails such as password resets and magic links — through a global mailer configured with environment variables.

    Set MAIL_PROVIDER to activate it. When unset, the default Resend mailer is used.

    Variable Example Description
    MAIL_PROVIDER smtp Global transport: smtp or resend. Leave unset to use defaults
    MAIL_FROM Intlayer <no-reply@acme.com> Sender header. Accepts a bare address or Name <email> format
    MAIL_SMTP_HOST smtp.acme.com SMTP host (required when MAIL_PROVIDER=smtp)
    MAIL_SMTP_PORT 587 SMTP port (defaults to 587)
    MAIL_SMTP_SECURE false Implicit 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
    Precedence: an organisation's own mailer (configured from the Organisation 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();
    

    Upgrading

    This pulls the latest images and restarts containers with docker compose pull && docker compose up -d. Existing volumes (mongo-data, redis-data, minio-data) are preserved — no data loss.

    sh
    docker compose pull
    docker compose up -d
    

    Backup and restore

    All persistent data lives in three named Docker volumes.

    Backup

    sh
    docker run --rm \
      -v intlayer_mongo-data:/data \
      -v "$(pwd)":/backup \
      busybox tar czf /backup/mongo-data.tar.gz /data
    
    docker run --rm \
      -v intlayer_redis-data:/data \
      -v "$(pwd)":/backup \
      busybox tar czf /backup/redis-data.tar.gz /data
    
    docker run --rm \
      -v intlayer_minio-data:/data \
      -v "$(pwd)":/backup \
      busybox tar czf /backup/minio-data.tar.gz /data
    

    Restore

    sh
    docker run --rm \
      -v intlayer_mongo-data:/data \
      -v "$(pwd)":/backup \
      busybox tar xzf /backup/mongo-data.tar.gz -C /
    
    # Repeat for redis-data and minio-data
    

    Limitations

    • MongoDB must be external (Atlas). The backend connects only over mongodb+srv:// (built from DB_ID / DB_MDP / DB_CLUSTER), so a plain mongodb://host:27017 — including the container's own bundled mongod — cannot be used. Provide a MongoDB Atlas cluster.
    • No custom domain. All browser-facing VITE_* URLs are inlined into the app at build time, and the published image ships with localhost values. The dashboard must be accessed at http://localhost:3000; serving it on a public domain would require rebuilding the image with the target URLs baked in and is not supported out of the box.
    • Email requires a working mailer. First-run setup enforces email verification, so either RESEND_API_KEY or a global SMTP mailer (MAIL_PROVIDER=smtp + MAIL_SMTP_*) must be configured. After the first admin signs in, each organisation can also configure its own SMTP or Resend mailer from the dashboard.

    Troubleshooting

    Backend crash-loops on first start

    MongoDB and Redis must be healthy before the backend starts. The compose file uses depends_on with condition: service_healthy. If you see repeated backend restarts, check that the mongo and redis healthchecks pass:

    sh
    docker compose ps
    docker compose logs mongo
    docker compose logs redis
    

    Look for MongoDB connection error near the top of the log.

    Dashboard cannot reach the API

    Verify that VITE_BACKEND_URL matches the URL where the backend is reachable from the browser (not the Docker network). If you changed the backend port or added a reverse proxy, rebuild the dashboard image:

    MinIO bucket missing

    If the minio-init one-shot service didn't run (or ran before MinIO was ready), create the bucket manually:

    sh
    docker compose run --rm minio-init