Self-Hosting a Logseq Sync Server

Local access with Tailscale, remote access with a reverse proxy

Logseq has undergone some transformative changes over the past few years, with the switch to the Database (DB) version that brings better performance, structured data entry, Real Time Collaboration (RTC) and a host of other features. The move away from storing notes in local markdown files also means that users can no longer rely on solutions like cloud folders (Google Drive, iCloud etc.) or Syncthing for syncing their notes across devices but now have to rely on Logseq’s official Sync services to enable cross-device access.

The official Logseq Sync service is a paid service that can currently be enabled by contributing to Logseq’s Open Collective at the “Backers” and “Sponsors” tiers. However, in the true spirit of Open Source, the developers have gracefully provided the infrastructure to self-host the Logseq Sync and Publish servers on our own hardware and the community has also built Docker containers (yshalsager’s logseq-selfhost) to make the self-hosted setup simpler.

This post walks through two ways of exposing that server:

  • Private access over Tailscale: the quickest way to sync between your own devices without exposing the server to the public internet.
  • Public access through a reverse proxy: useful when you need to sync from a device that cannot run Tailscale, such as a managed work computer.

Both approaches sit on top of the same logseq-selfhost containers: choose the Tailscale route if you’re syncing between your own devices, or the reverse proxy route if you need access from somewhere Tailscale can’t reach, like a managed work computer.

Common Prerequisites

Whichever access route you choose, you will need:

  • Logseq account: Self-hosting replaces Logseq’s hosted Sync and Publish endpoints, but the current clients still require you to sign in with a Logseq account for authentication. You do not need a subscription while using the self-hosted route, only the account.
  • Device capable of running Docker: A home server, NAS, desktop computer, or VPS.
  • Docker Engine with Compose: On Linux, follow Docker’s official install guide; On MacOS and Windows, Docker Desktop includes the Compose plugin
  • Git: For cloning the logseq-selfhost repository
  • Logseq DB clients: Client application for every device you want to sync

Creating a Logseq Account

A Logseq account can be created using any of Logseq’s client applications or by navigating to test.logseq.com Login Sign Up.

The account created here will be used to authenticate all other clients for the sync operations.

Private access over Tailscale

Tailscale creates a private mesh network (a “tailnet”) between your devices, so the sync server never needs a public IP, a domain, or a certificate: you just point your Logseq clients at the server’s Tailscale IP address.

Ensure Tailscale is installed and signed in on the server and on every device you want to sync from.

Setup

  1. Install Tailscale on the server and on each client device, then note the Tailscale IP address assigned to the server (visible in the Tailscale admin console, or by running tailscale ip -4 on the server itself).
The server's Tailscale IP, as shown in the admin console.
  1. Clone the repository and prepare the environment file:

    git clone https://github.com/yshalsager/logseq-selfhost.git
    cd logseq-selfhost/images/sync
    cp .env.example .env
    
  2. Point the sync server at your Tailscale IP. Open .env and change:

    DB_SYNC_BASE_URL=https://sync.example.com
    

    to:

    DB_SYNC_BASE_URL=http://YOUR_TAILSCALE_IP:8787
    
  3. Start the server:

    docker compose pull
    docker compose up -d
    
  4. Verify it’s running:

    curl http://localhost:8787/health
    

    should return:

    {"ok":true}
    

Once the health endpoint is reachable from another device on your tailnet, continue to Connecting your Logseq clients and use:

http://YOUR_TAILSCALE_IP:8787

You can optionally enable MagicDNS in Tailscale and use a memorable address such as http://my-server:8787 instead of the server’s Tailscale IP.

One thing worth flagging: the sync server listens on port 8787 by default, and Tailscale does not close that port to the rest of your network, it only gives your devices a private way to reach it. If your server also has a LAN or public IP, take a moment to confirm nothing outside your tailnet can reach port 8787, either with a firewall rule or by checking your router is not forwarding it.

Most firewalls can scope that rule to Tailscale’s own IP range (100.64.0.0/10) rather than opening the port to everyone.

Public access through a reverse proxy

If you want to sync from outside your tailnet, or cannot install Tailscale on a device, you can place the same Sync server behind NGINX Proxy Manager (NPM) and access it through a normal HTTPS domain.

I chose NGINX Proxy Manager (NPM) as I already have a server stack running with NPM to connect to. NPM can be replaced with your reverse-proxy manager of choice.

Prerequisites

  • NGINX Proxy Manager already installed and running, with a Docker network that the Logseq container can join.
  • A domain with a DNS provider, with the ability to create and edit DNS records (this guide uses Cloudflare)
  • Ports 80 and 443 forwarded to NGINX Proxy Manager from your router or firewall.

Setup

  1. Create a deployment directory containing docker-compose.yml and .env, with persistent data stored in a ./data subdirectory:

    mkdir -p ~/home-server/logseq-sync/data
    cd ~/home-server/logseq-sync
    

    Additional config for Linux Users

    Linux bind-mount permissions: The container runs as the unprivileged user nonroot with UID/GID 65532:65532. On native Linux, or when the deployment directory is stored inside the WSL Linux filesystem, the bind-mounted data directory may need to be owned by this UID:

    sudo chown -R 65532:65532 ~/home-server/logseq-sync/data
    sudo chmod 750 ~/home-server/logseq-sync/data
    

    Docker Desktop users on macOS, and Windows users mounting a folder from the Windows filesystem, normally do not need this step. Keep the directory owned by your normal host user unless the container reports a permission error.

  2. Create the environment file (.env):

    DB_SYNC_BASE_URL=https://logseq-sync.example.com
    DB_SYNC_ADMIN_TOKEN=replace-with-random-token
    

    Generate the token with:

    openssl rand -hex 32
    
  3. Create docker-compose.yml:

    name: logseq-selfhost-sync
    
    services:
      logseq-sync:
        image: ghcr.io/yshalsager/logseq-selfhost-sync:latest
        container_name: logseq-selfhost-sync
        restart: unless-stopped
        pull_policy: always
    
        environment:
          DB_SYNC_PORT: "8787"
          DB_SYNC_BASE_URL: "${DB_SYNC_BASE_URL}"
          DB_SYNC_DATA_DIR: "/app/data"
          DB_SYNC_STORAGE_DRIVER: "sqlite"
          DB_SYNC_ASSETS_DRIVER: "filesystem"
          DB_SYNC_LOG_LEVEL: "info"
    
          COGNITO_ISSUER: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
          COGNITO_CLIENT_ID: "69cs1lgme7p8kbgld8n5kseii6"
          COGNITO_JWKS_URL: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8/.well-known/jwks.json"
    
          DB_SYNC_ADMIN_TOKEN: "${DB_SYNC_ADMIN_TOKEN}"
    
        volumes:
          - ./data:/app/data
    
        read_only: true
    
        tmpfs:
          - /tmp
    
        cap_drop:
          - ALL
    
        security_opt:
          - no-new-privileges:true
    
        networks:
          - default
          - nginx-proxy-manager_default
    
    networks:
      nginx-proxy-manager_default:
        external: true
    

    This Compose configuration makes two deliberate changes from the upstream yshalsager’s logseq-selfhost example:

    1. ./data:/app/data is a bind mount, so the sync database and uploaded assets are stored in the data directory beside the Compose file rather than in a Docker-managed volume. The data therefore persists when the container is recreated and can be included in your normal server backups.
    2. The service has no ports: section, so port 8787 is not published directly on the Docker host. Instead, the Logseq container joins the same Docker network as NGINX Proxy Manager, which can reach it internally at logseq-selfhost-sync:8787. Only NGINX Proxy Manager needs to expose ports 80 and 443 to the host and the wider network.

    The network name must match the Docker network used by your NPM deployment. You can find it by running:

    docker network ls
    
  4. Set up DNS. Create a record pointing at your infrastructure:

    logseq-sync.example.com
    
    Adding a CNAME record for the Sync subdomain in the Cloudflare dashboard.
  5. Start the container:

    docker compose pull
    docker compose up -d
    
  6. Create a proxy host in NGINX Proxy Manager:

    Setting Value
    Domain name logseq-sync.example.com
    Scheme http
    Forward hostname logseq-selfhost-sync
    Forward port 8787


Enable Websockets Support and Block Common Exploits

The Details tab for the Sync proxy host, with Block Common Exploits and Websockets Support enabled.

Under the “SSL” tab, enable Force SSL and HTTP/2 Support and request a new certificate:

The SSL tab for the Sync proxy host, with Force SSL and HTTP/2 Support enabled.

Under Advanced (the settings icon), add:

client_max_body_size 1024m;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
The Advanced tab for the Sync proxy host, with the custom NGINX configuration applied.

The longer proxy_read_timeout/proxy_send_timeout values matter here: Logseq Sync operations on large graphs can take a while, and NPM’s defaults will otherwise cut the connection early.

Verification

docker compose ps

curl -i https://logseq-sync.example.com/health

should return:

{"ok":true}

Once the public health endpoint is working, continue to Connecting your Logseq clients and use:

https://logseq-sync.example.com

A note on security

If you adapt this compose file for your own setup, keep the hardening defaults intact: read-only filesystems, dropped capabilities, and no ports published directly on the host. Together they mean the container has little room to do anything beyond running the sync service, even if something in it were compromised.

Alternative: Cloudflare Tunnel without port forwarding

Some internet providers use carrier-grade NAT or otherwise prevent customers from forwarding ports on their router. In that situation, a Cloudflare Tunnel can publish the Sync server without opening ports 80, 443, or 8787.

Instead of accepting an inbound connection from the internet, a small cloudflared container establishes an outbound connection to Cloudflare. Requests to your Sync hostname travel through that tunnel and are forwarded directly to the Logseq container over Docker’s internal network.

This method does not require NGINX Proxy Manager, but it reuses the same deployment directory, bind-mount, and .env pattern from steps 1–2 of the reverse-proxy setup above — including the Linux permissions note if you’re on native Linux or WSL. Create that directory and .env file first if you haven’t already.

Prerequisites

You will need:

  • A Cloudflare account.
  • A domain using Cloudflare DNS.
  • A server capable of making outbound internet connections.
  • Docker Engine with Compose.
1. Create the tunnel

Open the Cloudflare dashboard and navigate to:

NetworkingTunnelsCreate tunnel

Give the tunnel a descriptive name, such as:

logseq-sync

Choose Docker as the deployment environment. Cloudflare will display a command containing a long tunnel token after the --token argument of the command

Naming the tunnel, selecting Docker as the environment, and confirming a successful connection in the Cloudflare dashboard.

Copy only the token and add it to the .env file alongside the variables from step 2 above:

CLOUDFLARE_TUNNEL_TOKEN=<Copied from the docker run command>

Treat the tunnel token as a password. Do not commit the .env file to Git or publish it in your Compose file.

2. Create the Compose configuration

Start from the docker-compose.yml in step 3 above. Change the logseq-sync service’s networks: entry to logseq-tunnel, drop nginx-proxy-manager_default, and add a cloudflared service:

services:
  logseq-sync:
    # ...unchanged from step 3 above...

    networks:
      - logseq-tunnel

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: logseq-cloudflared
    restart: unless-stopped
    pull_policy: always

    command:
      - tunnel
      - --no-autoupdate
      - run

    environment:
      TUNNEL_TOKEN: "${CLOUDFLARE_TUNNEL_TOKEN}"

    depends_on:
      - logseq-sync

    networks:
      - logseq-tunnel

networks:
  logseq-tunnel:

This configuration deliberately contains no ports: section. Neither container publishes a port on the Docker host; logseq-sync and cloudflared communicate only through the private logseq-tunnel Docker network.

3. Configure the published hostname

Return to the tunnel in the Cloudflare dashboard and add a Published application route.

Use the following values:

Setting Value
Subdomain logseq-sync
Domain example.com
Path (leave empty)
Service URL http://logseq-sync:8787
Adding the published application route in the Cloudflare dashboard.

The service URL uses the Docker Compose service name, not the server’s LAN address. Because cloudflared and logseq-sync share the same Docker network, cloudflared can resolve that name internally.

The public URL must match DB_SYNC_BASE_URL:

https://logseq-sync.example.com

Cloudflare terminates the public HTTPS connection. Communication between cloudflared and the Logseq container remains internal to the Docker host and therefore uses HTTP.

4. Start the containers

From the deployment directory, run:

docker compose pull
docker compose up -d

Check that both containers are running:

docker compose ps

You can inspect the tunnel connection with:

docker compose logs cloudflared
5. Test the public endpoint

Test the health endpoint through Cloudflare:

curl https://logseq-sync.example.com/health

It should return:

{ "ok": true }

You can then enter the following as the custom Sync server URL in Logseq:

https://logseq-sync.example.com

A Cloudflare Tunnel avoids opening inbound ports, but the hostname is still publicly reachable through Cloudflare. Authentication is still provided by the Logseq account and Cognito configuration; the tunnel itself should not be treated as an authentication mechanism.

Cloudflare Tunnel supports WebSockets automatically, so there is no equivalent of NGINX Proxy Manager’s Websockets Support switch to enable.

Avoid placing an interactive Cloudflare Access login page in front of the Sync hostname unless you have verified that the Logseq client can complete that authentication flow. A browser-based challenge may prevent the desktop or mobile client from reaching the Sync API.

Optional: bypass Cloudflare caching

The Sync hostname serves changing API data rather than a conventional static website. To avoid unexpected caching behaviour, create a Cloudflare Cache Rule matching:

Hostname equals logseq-sync.example.com

and set:

Cache eligibility: Bypass cache
Upload-size limitation

Traffic sent through a Cloudflare Tunnel is subject to Cloudflare’s proxied request-size limits. Free and Pro zones currently allow request bodies of up to 100 MB; larger individual asset uploads may return a 413 Payload Too Large response.

Connecting your Logseq clients

The client setup is the same for both access routes. The only difference is the Sync server URL:

Access route Sync server URL
Tailscale http://YOUR_TAILSCALE_IP:8787
Reverse proxy https://logseq-sync.example.com

Use the appropriate URL on every device you want to synchronize.

Desktop

The latest Desktop client builds can be downloaded from Logseq’s GitHub: choose the installer appropriate for your OS.

In Logseq, login with your Logseq account (see Creating a Logseq Account), then go to Settings → Advanced → Sync Server URL and enter your Sync server URL.

Setting the custom Sync Server URL in Logseq's desktop settings.

If successful, you should see a cloud icon with a green dot signalling the sync status in the top right corner, next to . Clicking on it will display the current sync status.

iPhone and iPad

The iOS/iPadOS clients for Logseq DB can be downloaded by joining the TestFlight program for the Logseq Beta app.

In Logseq, login with your Logseq account (see Creating a Logseq Account), then add your server url to the Sync Server URL setting.

Setting the custom Sync Server URL in Logseq for iOS and iPadOS.

If successful, you should see a cloud icon with a green dot () signalling the sync status in the top right corner, next to . Clicking on it will display the current sync status.

Android

The latest Desktop client builds can be downloaded from Logseq’s GitHub: choose the .apk file from the Assets list.

In Logseq, login with your Logseq account (see Creating a Logseq Account), then add your server url to the Sync Server URL setting.

Setting the custom Sync Server URL in Logseq for Android.

If successful, you should see a cloud icon with a green dot () signalling the sync status in the top right corner, next to . Clicking on it will display the current sync status.

If using Tailscale, install Tailscale and sign in to the same tailnet.

On every device, make sure Use Logseq Sync Beta is enabled before creating a new graph or synchronizing an existing one.

Optional: Web and Publish servers

The Sync server is all you need to synchronize a DB graph. The same logseq-selfhost project also provides two optional services:

  • logseq-selfhost-web — the web application interface.
  • logseq-selfhost-publish — the Publish server for sharing a graph.

If you need either feature, replace the Sync-only deployment with the full Sync + Web + Publish stack in my self-hosted-docker-setups repository. The expanded stack uses the same ./data directory for Sync and attaches all three containers to NPM’s network.

Create two additional DNS records:

  • logseq-web.example.com
  • logseq-publish.example.com

Then create proxy hosts pointing to:

Service Forward hostname Internal port
Web logseq-selfhost-web 8080
Publish logseq-selfhost-publish 8787

For both hosts, enable Force SSL and HTTP/2 Support. The Web proxy can use the same timeout configuration as Sync, while the Publish proxy should also allow long-running requests.

In Logseq, set:

  • Publish Server URL: https://logseq-publish.example.com

While the Publish server can also be exposed over Tailscale, the published pages will only be accessible on devices connected to your tailnet.

You can see the Publish server in action in the previous version of this guide, which is hosted using the same setup. That version also documents the complete combined Sync, Web, and Publish stack behind NGINX Proxy Manager.

For most personal setups, Tailscale is the simplest place to start. If you later need the reverse proxy route or want to publish Logseq DB pages publicly, replace the Tailscale deployment with the reverse proxy route.




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • Docker for Bioinformatics
  • A Bioinformatician's Toolkit
  • Simulations for Biological Inference
  • Beats of Stress