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
| Project | SiYuan (b3log/siyuan) |
| License | AGPLv3 |
| Recommended Plan | RamNode KVM 1–2 GB (2 GB+ for large workspaces) |
| OS | Ubuntu 22.04/24.04 or Debian 12 |
| Stack | Docker + Nginx + Certbot + Restic |
| Estimated Setup Time | 30 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
Initial Server Hardening
sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw fail2ban curl ca-certificates gnupg lsb-releasesudo 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 enablePermitRootLogin no
PasswordAuthentication noReload with sudo systemctl reload ssh — verify your SSH key works in a second terminal first.
Install Docker Engine
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 $USERLog out and back in. Verify with docker version and docker compose version.
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.
sudo mkdir -p /siyuan/workspace
sudo chown -R 1000:1000 /siyuan/workspace
sudo chmod 755 /siyuanGenerate an Access Auth Code
SiYuan has no user accounts — a single accessAuthCode gates the kernel for all clients. Treat it like a database password.
openssl rand -base64 32Create the Docker Compose Stack
sudo mkdir -p /opt/siyuan-stack
sudo chown $USER:$USER /opt/siyuan-stack
cd /opt/siyuan-stackSIYUAN_AUTH_CODE=YOUR_GENERATED_CODE_HERE
TZ=America/New_Yorkchmod 600 /opt/siyuan-stack/.envservices:
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: 3cd /opt/siyuan-stack
docker compose up -d
docker compose logs -f siyuanCritical: bind to 127.0.0.1:6806, not 0.0.0.0:6806 — otherwise the kernel is exposed directly and bypasses the reverse proxy.
Nginx with Let's Encrypt and WebSocket Support
sudo apt install -y nginx python3-certbot-nginxmap $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;
}
}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-runTwo 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.
Encrypted Backups with Restic
sudo apt install -y restic
sudo mkdir -p /etc/restic && sudo chmod 700 /etc/resticRESTIC_REPOSITORY=sftp:user@backup.example.com:/backups/siyuan
RESTIC_PASSWORD=USE_A_LONG_RANDOM_PASSPHRASE_HEREsudo bash -c 'set -a; . /etc/restic/siyuan.env; restic init'#!/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%# /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.targetsudo chmod 750 /usr/local/sbin/siyuan-backup
sudo systemctl daemon-reload
sudo systemctl enable --now siyuan-backup.timerA backup you've never restored is not a backup. Test once before you actually need one.
Updates
cd /opt/siyuan-stack
docker compose pull
docker compose up -dRun a backup immediately before pulling — SiYuan has occasionally introduced workspace migrations that aren't trivially reversible. Watch the GitHub release notes for breaking changes.
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://…/wsin browser dev tools. Almost always missingproxy_http_version 1.1or 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.
