Knowledge Base
    Open Source

    Deploy SiYuan on a VPS

    A privacy-first, fully open-source personal knowledge base — Notion-style block references and graph view, running on infrastructure you own, with browser, desktop, and mobile clients hitting the same workspace.

    At a Glance

    ProjectSiYuan (b3log/siyuan)
    LicenseAGPLv3
    Recommended PlanRamNode KVM 1–2 GB (2 GB+ for large workspaces)
    OSUbuntu 22.04/24.04 or Debian 12
    StackDocker + Nginx + Certbot + Restic
    Estimated Setup Time30 minutes

    Prerequisites

    • A RamNode KVM VPS (1–2 GB recommended)
    • SSH access as a non-root sudo user
    • A registered domain with an A record pointing at the VPS IPv4
    • ~30 minutes start to finish
    1

    Initial Server Hardening

    Update and install baseline packages
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y ufw fail2ban curl ca-certificates gnupg lsb-release
    Firewall — never expose 6806 directly
    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable
    Lock down SSH (/etc/ssh/sshd_config)
    PermitRootLogin no
    PasswordAuthentication no

    Reload with sudo systemctl reload ssh — verify your SSH key works in a second terminal first.

    2

    Install Docker Engine

    Add Docker's official repo
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
        -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
      https://download.docker.com/linux/ubuntu \
      $(. /etc/os-release && echo \"${VERSION_CODENAME}\") stable" | \
      sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo usermod -aG docker $USER

    Log out and back in. Verify with docker version and docker compose version.

    3

    Prepare the SiYuan Workspace

    The container runs as UID/GID 1000:1000 by default. Use the same path on host and container so kernel log paths match.

    Create and chown the workspace
    sudo mkdir -p /siyuan/workspace
    sudo chown -R 1000:1000 /siyuan/workspace
    sudo chmod 755 /siyuan
    4

    Generate an Access Auth Code

    SiYuan has no user accounts — a single accessAuthCode gates the kernel for all clients. Treat it like a database password.

    Generate a 32-byte random code
    openssl rand -base64 32
    5

    Create the Docker Compose Stack

    Stack directory and .env
    sudo mkdir -p /opt/siyuan-stack
    sudo chown $USER:$USER /opt/siyuan-stack
    cd /opt/siyuan-stack
    /opt/siyuan-stack/.env
    SIYUAN_AUTH_CODE=YOUR_GENERATED_CODE_HERE
    TZ=America/New_York
    Lock down .env
    chmod 600 /opt/siyuan-stack/.env
    /opt/siyuan-stack/docker-compose.yml
    services:
      siyuan:
        image: b3log/siyuan:latest
        container_name: siyuan
        restart: unless-stopped
        user: "1000:1000"
        command:
          - "--workspace=/siyuan/workspace/"
          - "--accessAuthCode=${SIYUAN_AUTH_CODE}"
        ports:
          # Bind localhost-only — Nginx is the only client.
          - "127.0.0.1:6806:6806"
        volumes:
          - /siyuan/workspace:/siyuan/workspace
        environment:
          - TZ=${TZ}
        healthcheck:
          test: ["CMD", "wget", "-qO-", "http://127.0.0.1:6806/"]
          interval: 30s
          timeout: 5s
          retries: 3
    Bring it up
    cd /opt/siyuan-stack
    docker compose up -d
    docker compose logs -f siyuan

    Critical: bind to 127.0.0.1:6806, not 0.0.0.0:6806 — otherwise the kernel is exposed directly and bypasses the reverse proxy.

    6

    Nginx with Let's Encrypt and WebSocket Support

    Install Nginx + Certbot
    sudo apt install -y nginx python3-certbot-nginx
    /etc/nginx/sites-available/siyuan.conf
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
    
    server {
        listen 80;
        listen [::]:80;
        server_name notes.example.com;
    
        location /.well-known/acme-challenge/ { root /var/www/html; }
        location / { return 301 https://$host$request_uri; }
    }
    
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        http2 on;
        server_name notes.example.com;
    
        ssl_certificate     /etc/letsencrypt/live/notes.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/notes.example.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
    
        client_max_body_size 1024m;
    
        # SiYuan WebSocket — required for live editor sync.
        location /ws {
            proxy_pass http://127.0.0.1:6806;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            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 $scheme;
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
        }
    
        # HTTP API, static assets, the web UI.
        # Do NOT add URL rewrites — they break SiYuan auth.
        location / {
            proxy_pass http://127.0.0.1:6806;
            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 $scheme;
            proxy_buffering off;
            proxy_request_buffering off;
            proxy_read_timeout 600s;
            proxy_send_timeout 600s;
        }
    }
    Enable and issue cert
    sudo mkdir -p /var/www/html
    sudo ln -s /etc/nginx/sites-available/siyuan.conf /etc/nginx/sites-enabled/
    sudo rm -f /etc/nginx/sites-enabled/default
    sudo certbot --nginx -d notes.example.com \
        --redirect --agree-tos --email you@example.com --no-eff-email
    sudo certbot renew --dry-run

    Two things easy to get wrong: the /ws block (WebSocket failure means the editor loads but never saves), and any URL rewrite (breaks auth). Always use a dedicated subdomain — never a subpath.

    7

    Encrypted Backups with Restic

    Install and initialize
    sudo apt install -y restic
    sudo mkdir -p /etc/restic && sudo chmod 700 /etc/restic
    /etc/restic/siyuan.env
    RESTIC_REPOSITORY=sftp:user@backup.example.com:/backups/siyuan
    RESTIC_PASSWORD=USE_A_LONG_RANDOM_PASSPHRASE_HERE
    Initialize the repo (one time)
    sudo bash -c 'set -a; . /etc/restic/siyuan.env; restic init'
    /usr/local/sbin/siyuan-backup
    #!/usr/bin/env bash
    set -euo pipefail
    source /etc/restic/siyuan.env
    
    # Quiesce the kernel so we don't snapshot a half-flushed write.
    docker compose -f /opt/siyuan-stack/docker-compose.yml stop siyuan
    restic backup /siyuan/workspace --tag siyuan --tag daily
    docker compose -f /opt/siyuan-stack/docker-compose.yml start siyuan
    
    restic forget --prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6
    restic check --read-data-subset=5%
    Schedule via systemd timer
    # /etc/systemd/system/siyuan-backup.service
    [Unit]
    Description=Restic backup of SiYuan workspace
    After=docker.service
    Requires=docker.service
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/sbin/siyuan-backup
    Nice=10
    IOSchedulingClass=best-effort
    IOSchedulingPriority=7
    
    # /etc/systemd/system/siyuan-backup.timer
    [Unit]
    Description=Nightly SiYuan backup
    
    [Timer]
    OnCalendar=*-*-* 03:30:00
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    Enable
    sudo chmod 750 /usr/local/sbin/siyuan-backup
    sudo systemctl daemon-reload
    sudo systemctl enable --now siyuan-backup.timer

    A backup you've never restored is not a backup. Test once before you actually need one.

    8

    Updates

    Two-step update
    cd /opt/siyuan-stack
    docker compose pull
    docker compose up -d

    Run a backup immediately before pulling — SiYuan has occasionally introduced workspace migrations that aren't trivially reversible. Watch the GitHub release notes for breaking changes.

    9

    Connect Browser, Desktop, and Mobile Clients

    • Browser: open https://notes.example.com/ and enter the auth code.
    • Desktop (macOS/Windows/Linux): install from siyuan-note.com → "Connect to remote kernel" → enter https://notes.example.com (HTTPS, no trailing slash, no port) and the auth code.
    • Mobile (iOS/Android): same pattern — pick the RamNode location closest to where you actually live or work; the WebSocket is doing real work here.
    • Single-user only: two devices editing the same document simultaneously will fight each other (last-writer-wins). For multi-user collaboration, look at HedgeDoc or Outline instead.

    Troubleshooting

    • Editor never finishes initializing: WebSocket failure — check wss://…/ws in browser dev tools. Almost always missing proxy_http_version 1.1 or the Upgrade headers.
    • Auth code rejected when correct: you've added a URL rewrite or a Cloudflare Page Rule rewriting URLs — remove it.
    • "Permission denied" on workspace files: container UID doesn't match host ownership — sudo chown -R 1000:1000 /siyuan/workspace, then restart.
    • "Kernel offline" intermittently: bump proxy_read_timeout; the 3600s value above is generous.