> Last updated: 2026-09-06  
> Server: Alibaba Cloud ECS / Ubuntu 24.04.4 LTS  
> Domain: `cltech.xin`

---

# 1. Server Environment

## ECS

- OS: Ubuntu 24.04.4 LTS
    
- CPU: 2 vCPU
    
- RAM: ~1.6 GiB
    
- Swap: 4 GiB
    
- System disk: 40 GB
    
- Free space during setup: ~28 GB
    
- Kernel: `6.8.0-63-generic`
    
- Internal IP: `172.20.39.32`
    
- Public IP: `8.130.215.143`
    

Installed / available:

```bash
git --version
# git 2.43.0

nginx -v
# nginx/1.24.0

rclone version
# rclone v1.60.1-DEV
```

> NOTE: rclone version is old and is a development build.  
> It is currently working and should NOT be upgraded casually because both Obsidian and Zotero WebDAV depend on it.

---

# 2. NAS Mount

NAS is mounted at:

```text
/mnt/nas
```

Main Obsidian storage:

```text
/mnt/nas/obsidian
```

Current Obsidian vault:

```text
/mnt/nas/obsidian/CL_base/
├── Deep_learning/
├── Hello/
├── Notes/
├── PSW.md
├── Private/
├── Untitled.base
├── Welcome.md
├── mRNA/
├── 装机/
└── Public/
```

Only `Public/` is intended for public website publishing.

Private content such as:

```text
Private/
Work/
```

must NOT be exposed through Quartz.

---

# 3. Obsidian Public Directory

Created:

```text
/mnt/nas/obsidian/CL_base/Public/
```

Current structure:

```text
Public/
├── research/
├── notes/
└── index.md
```

Quartz only reads this directory.

Important:

```text
/opt/quartz/content
```

is a symbolic link to:

```text
/mnt/nas/obsidian/CL_base/Public
```

Check:

```bash
ls -l /opt/quartz/content
```

Expected:

```text
/opt/quartz/content -> /mnt/nas/obsidian/CL_base/Public
```

---

# 4. Important Permission Configuration

## Problem encountered

Originally:

```text
/mnt/nas/obsidian/CL_base/Public
drwxr-xr-x root root
```

while rclone WebDAV runs as:

```text
User=obsidian
Group=obsidian
```

Therefore Obsidian could read files but could not write them.

Symptoms:

```text
GET       → 200
PROPFIND  → 207
PUT       → 405
```

rclone journal showed:

```text
permission denied
```

Specifically:

```text
open /mnt/nas/obsidian/CL_base/Public/index.md:
permission denied
```

## Fix

Changed ownership:

```bash
chown -R obsidian:obsidian /mnt/nas/obsidian/CL_base/Public
```

Current expected permissions:

```text
Public/
owner: obsidian
group: obsidian
```

Example:

```text
drwxr-xr-x obsidian obsidian Public
-rw-r--r-- obsidian obsidian index.md
```

This fixed the WebDAV PUT/405 problem.

## Permission test

To test write access:

```bash
sudo -u obsidian touch /mnt/nas/obsidian/CL_base/Public/.write-test
```

Then:

```bash
rm /mnt/nas/obsidian/CL_base/Public/.write-test
```

---

# 5. Obsidian WebDAV Sync

## Service

Systemd service:

```text
rclone-obsidian.service
```

Check:

```bash
systemctl status rclone-obsidian.service
```

Configuration:

```bash
systemctl cat rclone-obsidian.service
```

Current service:

```ini
[Unit]
Description=Rclone WebDAV for Obsidian

After=network-online.target
Wants=network-online.target

[Service]
Type=simple

User=obsidian
Group=obsidian

EnvironmentFile=/etc/rclone/obsidian.env

ExecStart=/usr/bin/rclone serve webdav /mnt/nas/obsidian \
    --addr 127.0.0.1:8080 \
    --user ${RCLONE_USER} \
    --pass ${RCLONE_PASS}

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Backend:

```text
127.0.0.1:8080
```

This is intentionally local-only.

External access goes through Nginx:

```text
https://sync.cltech.xin
```

---

# 6. Obsidian WebDAV Troubleshooting

When Obsidian Sync reported:

```text
405 Method Not Allowed
```

initially suspected:

- Nginx
    
- WebDAV configuration
    
- rclone
    
- file permissions
    
- Quartz
    

Tests showed:

```text
PROPFIND → 207
GET       → 200
PUT       → 405
```

Nginx access log showed:

```text
PUT /CL_base/Public/index.md → 405
```

Then PUT was tested directly against rclone:

```bash
curl -u 'obsidian:PASSWORD' \
  -T /tmp/rclone-test.txt \
  http://127.0.0.1:8080/CL_base/Public/rclone-test.txt \
  -i
```

It also returned:

```text
405 Method Not Allowed
```

Therefore Nginx was not the cause.

rclone journal revealed the actual cause:

```text
open /mnt/nas/obsidian/CL_base/Public/index.md:
permission denied
```

After:

```bash
chown -R obsidian:obsidian /mnt/nas/obsidian/CL_base/Public
```

WebDAV PUT succeeded.

Conclusion:

> The 405 was caused by filesystem permissions.  
> rclone was running as `obsidian`, while `Public/` was owned by `root`.

---

# 7. Obsidian WebDAV URL

External URL:

```text
https://sync.cltech.xin
```

Backend:

```text
127.0.0.1:8080
```

Do NOT expose port 8080 directly to the Internet.

---

# 8. Zotero WebDAV

Zotero WebDAV is a separate service and should not be modified when working on the Obsidian service.

External URL:

```text
https://zotero.cltech.xin
```

Backend:

```text
127.0.0.1:8081
```

rclone service:

```text
rclone-zotero.service
```

Check:

```bash
systemctl status rclone-zotero.service
```

Current process:

```text
/usr/bin/rclone serve webdav /mnt/nas/zotero \
    --addr 127.0.0.1:8081 \
    --user zotero \
    --pass ... \
    --realm "Zotero WebDAV" \
    --poll-interval 30s
```

Zotero WebDAV has been tested successfully.

A request such as:

```bash
curl -I https://zotero.cltech.xin
```

returns:

```text
HTTP/1.1 401 Unauthorized
```

This is expected because authentication is required.

Authenticated WebDAV operations work.

---

# 9. Nginx Architecture

Nginx is the external HTTPS entry point.

Architecture:

```text
Internet
   │
   │ HTTPS :443
   ▼
 Nginx
   │
   ├── www.cltech.xin
   │       │
   │       ▼
   │   /opt/quartz/public
   │
   ├── sync.cltech.xin
   │       │
   │       ▼
   │   127.0.0.1:8080
   │       │
   │       ▼
   │   rclone → /mnt/nas/obsidian
   │
   └── zotero.cltech.xin
           │
           ▼
       127.0.0.1:8081
           │
           ▼
       rclone → /mnt/nas/zotero
```

---

# 10. Quartz

Quartz version:

```text
v5.0.0
```

Installation directory:

```text
/opt/quartz
```

Content:

```text
/opt/quartz/content
```

is symlinked to:

```text
/mnt/nas/obsidian/CL_base/Public
```

Output:

```text
/opt/quartz/public
```

Build command:

```bash
cd /opt/quartz
npx quartz build
```

Typical build time:

```text
~5 seconds
```

Quartz successfully builds the Public directory.

---

# 11. Quartz Warnings

Current build may show:

```text
Could not load plugin "@quartz-community/obsidian-plugin-excalidraw"
```

This does not currently prevent the build.

Also:

```text
Warning: couldn't find git repository for content
```

This is expected because Quartz content is coming from the Obsidian Public directory rather than a Git repository.

Another warning:

```text
Warning: found invalid date "0" in "index.md"
```

This does not prevent the build.

These warnings were intentionally left unchanged during initial setup.

---

# 12. Quartz Preview

Default Quartz preview port `8080` conflicts with Obsidian WebDAV.

Therefore use:

```bash
npx quartz build --serve --port 3000
```

for local preview.

Port `3000` is only for development/preview.

Production website uses Nginx and:

```text
/opt/quartz/public
```

---

# 13. Website

Domain:

```text
www.cltech.xin
```

DNS currently points to:

```text
8.130.215.143
```

Check:

```bash
dig +short www.cltech.xin
```

Expected:

```text
8.130.215.143
```

No AAAA record was configured.

---

# 14. Production Nginx Configuration

Website configuration:

```nginx
server {
    listen 80;
    listen [::]:80;

    server_name www.cltech.xin;

    root /opt/quartz/public;

    location ^~ /.well-known/acme-challenge/ {
        try_files $uri =404;
    }

    location / {
        return 301 https://www.cltech.xin$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name www.cltech.xin;

    root /opt/quartz/public;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/www.cltech.xin/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.cltech.xin/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        try_files $uri $uri/ =404;
    }
}
```

After configuration:

```bash
nginx -t
systemctl reload nginx
```

Test:

```bash
curl -I https://www.cltech.xin
```

Expected:

```text
HTTP/1.1 200 OK
```

HTTP:

```bash
curl -I http://www.cltech.xin
```

Expected:

```text
HTTP/1.1 301 Moved Permanently
Location: https://www.cltech.xin/
```

---

# 15. HTTPS / Let's Encrypt

Certificate for:

```text
www.cltech.xin
```

was successfully obtained using DNS-01.

Certificate path:

```text
/etc/letsencrypt/live/www.cltech.xin/fullchain.pem
```

Private key:

```text
/etc/letsencrypt/live/www.cltech.xin/privkey.pem
```

Certificate expiry observed during setup:

```text
2026-12-05
```

DNS provider:

```text
Alibaba Cloud DNS
```

Certbot virtual environment:

```text
/opt/certbot-aliyun
```

Certbot version:

```text
certbot 5.8.0
```

Installed plugin:

```text
certbot-dns-aliyun
```

---

# 16. Certbot Credentials

Credentials file:

```text
/root/.secrets/certbot/aliyun.ini
```

Format:

```ini
dns_aliyun_access_key = ...
dns_aliyun_access_key_secret = ...
```

Permissions:

```bash
chmod 600 /root/.secrets/certbot/aliyun.ini
```

RAM user:

```text
quartz-certbot
```

Console login disabled.

The user was assigned DNS permissions for certificate DNS-01 validation.

IMPORTANT:

> Never paste the contents of `aliyun.ini` into chat, logs, screenshots, or public notes.

---

# 17. Certbot DNS-01 Command

Certificate was obtained with:

```bash
/opt/certbot-aliyun/bin/certbot certonly \
  --authenticator dns-aliyun \
  --dns-aliyun-credentials /root/.secrets/certbot/aliyun.ini \
  -d www.cltech.xin
```

Check certificates:

```bash
/opt/certbot-aliyun/bin/certbot certificates
```

---

# 18. Certbot Renewal Status

Dry-run:

```bash
/opt/certbot-aliyun/bin/certbot renew --dry-run
```

The `www.cltech.xin` renewal simulation succeeded.

Other existing certificates:

```text
sync.cltech.xin
zotero.cltech.xin
```

have older/different renewal configurations and produced failures during the global dry-run.

Do NOT modify the working Obsidian/Zotero services merely because those renewal configurations need later cleanup.

Future maintenance should handle these separately.

---

# 19. Automatic Quartz Publishing

Publishing script:

```text
/usr/local/bin/quartz-publish.sh
```

Current script:

```bash
#!/bin/bash

set -e

QUARTZ_DIR="/opt/quartz"
CONTENT_DIR="/mnt/nas/obsidian/CL_base/Public"
STAMP_FILE="/var/lib/quartz-public/.last-build"

mkdir -p "$(dirname "$STAMP_FILE")"

if [ ! -d "$CONTENT_DIR" ]; then
    echo "$(date '+%F %T') ERROR: Public directory not found"
    exit 1
fi

LATEST=$(find "$CONTENT_DIR" -type f -printf '%T@\n' 2>/dev/null | sort -n | tail -1)

if [ -z "$LATEST" ]; then
    echo "$(date '+%F %T') No files in Public"
    exit 0
fi

LAST=$(cat "$STAMP_FILE" 2>/dev/null || echo 0)

if [ "$LATEST" = "$LAST" ]; then
    exit 0
fi

echo "$(date '+%F %T') Changes detected, building Quartz..."

cd "$QUARTZ_DIR"
npx quartz build

echo "$LATEST" > "$STAMP_FILE"

echo "$(date '+%F %T') Quartz build completed."
```

Executable:

```bash
chmod +x /usr/local/bin/quartz-publish.sh
```

---

# 20. Quartz Publishing Systemd Service

File:

```text
/etc/systemd/system/quartz-publish.service
```

Configuration:

```ini
[Unit]
Description=Build Quartz website from Obsidian Public
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/quartz-publish.sh
```

Manual execution:

```bash
systemctl start quartz-publish.service
```

Check:

```bash
systemctl status quartz-publish.service --no-pager
```

---

# 21. Quartz Publishing Timer

File:

```text
/etc/systemd/system/quartz-publish.timer
```

Configuration:

```ini
[Unit]
Description=Check Obsidian Public and build Quartz every minute

[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
AccuracySec=10s
Persistent=true

[Install]
WantedBy=timers.target
```

Enable:

```bash
systemctl daemon-reload
systemctl enable --now quartz-publish.timer
```

Check:

```bash
systemctl status quartz-publish.timer --no-pager
```

The timer checks the Public directory approximately every minute.

If a file modification time changes:

```text
Public changed
      ↓
quartz-publish.service
      ↓
npx quartz build
      ↓
/opt/quartz/public
```

If nothing changed, Quartz is not rebuilt.

---

# 22. Final Publishing Architecture

Current production workflow:

```text
Obsidian
   │
   │ edit/save
   ▼
Obsidian Sync
   │
   │ HTTPS
   ▼
sync.cltech.xin
   │
   ▼
Nginx
   │
   ▼
127.0.0.1:8080
   │
   ▼
rclone WebDAV
   │
   ▼
/mnt/nas/obsidian/CL_base/Public
   │
   │ every ~1 minute
   ▼
quartz-publish.timer
   │
   ▼
npx quartz build
   │
   ▼
/opt/quartz/public
   │
   ▼
Nginx
   │
   ▼
https://www.cltech.xin
```

This means:

> Edit a Markdown file in Obsidian → Obsidian Sync uploads it → NAS receives it → Quartz automatically rebuilds → website updates.

---

# 23. Important Security Boundary

The public website must only use:

```text
/mnt/nas/obsidian/CL_base/Public
```

Quartz must NOT point to:

```text
/mnt/nas/obsidian/CL_base
```

because that would potentially expose:

```text
Private/
PSW.md
Work/
other personal files
```

Current safe configuration:

```text
Quartz content
      ↓
/mnt/nas/obsidian/CL_base/Public
```

---

# 24. Useful Maintenance Commands

## Check website

```bash
curl -I https://www.cltech.xin
```

## Check website HTTP redirect

```bash
curl -I http://www.cltech.xin
```

## Check DNS

```bash
dig +short www.cltech.xin
```

## Test Nginx configuration

```bash
nginx -t
```

## Reload Nginx

```bash
systemctl reload nginx
```

## Check Obsidian WebDAV

```bash
systemctl status rclone-obsidian.service --no-pager
```

## Check Zotero WebDAV

```bash
systemctl status rclone-zotero.service --no-pager
```

## Check Quartz timer

```bash
systemctl status quartz-publish.timer --no-pager
```

## Check publishing logs

```bash
journalctl -u quartz-publish.service --since "30 min ago" --no-pager
```

## Check Obsidian WebDAV logs

```bash
journalctl -u rclone-obsidian.service --since "30 min ago" --no-pager
```

## Check Quartz build manually

```bash
cd /opt/quartz
npx quartz build
```

---

# 25. If Obsidian Sync Breaks Again

Use this diagnostic order.

### Step 1 — Check service

```bash
systemctl status rclone-obsidian.service --no-pager
```

### Step 2 — Check logs

```bash
journalctl -u rclone-obsidian.service --since "10 min ago" --no-pager
```

Look for:

```text
permission denied
```

### Step 3 — Check Public permissions

```bash
ls -ld /mnt/nas/obsidian/CL_base/Public
ls -l /mnt/nas/obsidian/CL_base/Public/
```

Expected owner:

```text
obsidian obsidian
```

### Step 4 — Test local write

```bash
sudo -u obsidian touch /mnt/nas/obsidian/CL_base/Public/.write-test
```

Then:

```bash
rm /mnt/nas/obsidian/CL_base/Public/.write-test
```

### Step 5 — Test WebDAV PUT

```bash
echo 'rclone-put-test' > /tmp/rclone-test.txt

curl -u 'obsidian:PASSWORD' \
  -T /tmp/rclone-test.txt \
  http://127.0.0.1:8080/CL_base/Public/rclone-test.txt \
  -i
```

A successful PUT should return a success status rather than:

```text
405 Method Not Allowed
```

Remove test file:

```bash
rm /mnt/nas/obsidian/CL_base/Public/rclone-test.txt
```

### Step 6 — Only then investigate Nginx

Do not immediately change Nginx if the local rclone PUT already fails.

---

# 26. If Website Stops Updating

Check:

```bash
systemctl status quartz-publish.timer --no-pager
```

Then:

```bash
journalctl -u quartz-publish.service --since "30 min ago" --no-pager
```

Check:

```bash
ls -l /mnt/nas/obsidian/CL_base/Public/
```

Check Quartz content:

```bash
ls -l /opt/quartz/content
```

Expected:

```text
/opt/quartz/content -> /mnt/nas/obsidian/CL_base/Public
```

Manual rebuild:

```bash
cd /opt/quartz
npx quartz build
```

Then:

```bash
curl -I https://www.cltech.xin
```

---

# 27. rclone Upgrade — Future Maintenance

Current version:

```text
rclone v1.60.1-DEV
```

This is old.

However:

> Do NOT upgrade simply because it is old.

Both services depend on the same `/usr/bin/rclone` binary:

```text
rclone-obsidian.service
rclone-zotero.service
```

A future upgrade should therefore be treated as a separate maintenance task.

Recommended procedure:

```text
1. Record current rclone version
2. Back up service files
3. Install newer stable rclone
4. Verify binary
5. Restart Obsidian service
6. Test GET
7. Test PROPFIND
8. Test PUT
9. Test DELETE
10. Test Obsidian Sync
11. Test Zotero WebDAV
12. Only then consider removing old binary
```

Do not combine a rclone upgrade with other major infrastructure changes.

---

# 28. Credentials Security

During troubleshooting, WebDAV credentials appeared in process output:

```bash
ps aux
```

This can expose:

```text
--pass ...
```

Therefore:

> Never paste complete `ps aux` output containing credentials into public notes or future troubleshooting logs.

Current credentials should be treated as secrets.

Credential files should have restrictive permissions:

```bash
chmod 600 /root/.secrets/certbot/aliyun.ini
```

Do not store passwords inside the public Quartz `Public/` directory.

---

# 29. Current Service Map

|Service|External URL|Backend|Data|
|---|---|---|---|
|Website|`https://www.cltech.xin`|Nginx → `/opt/quartz/public`|Quartz static site|
|Obsidian Sync|`https://sync.cltech.xin`|`127.0.0.1:8080`|`/mnt/nas/obsidian`|
|Zotero WebDAV|`https://zotero.cltech.xin`|`127.0.0.1:8081`|`/mnt/nas/zotero`|

Systemd:

|Service|Purpose|
|---|---|
|`rclone-obsidian.service`|Obsidian WebDAV|
|`rclone-zotero.service`|Zotero WebDAV|
|`quartz-publish.service`|Quartz build|
|`quartz-publish.timer`|Automatic build check|

---

# 30. Current Status — 2026-09-06

## Working

-  Alibaba Cloud ECS
    
-  Ubuntu 24.04
    
-  NAS mounted
    
-  Obsidian WebDAV
    
-  Zotero WebDAV
    
-  `sync.cltech.xin`
    
-  `zotero.cltech.xin`
    
-  `www.cltech.xin`
    
-  HTTPS for website
    
-  Let's Encrypt DNS-01
    
-  Quartz v5.0.0
    
-  Obsidian Public directory
    
-  Quartz content symlink
    
-  Automatic Quartz build
    
-  Systemd publishing timer
    
-  Obsidian WebDAV PUT
    
-  Obsidian `index.md` synchronization
    
-  Website automatic publishing pipeline
    

## Known non-critical items

-  rclone is old: `v1.60.1-DEV`
    
-  `sync.cltech.xin` certificate renewal configuration should be reviewed later
    
-  `zotero.cltech.xin` certificate renewal configuration should be reviewed later
    
-  Quartz Excalidraw plugin warning remains
    
-  Quartz invalid date `0` warning remains
    
-  Quartz content Git repository warning remains
    

These items do not currently prevent the website or synchronization system from working.

---

# 31. Planned Website Development

The infrastructure is now ready for the next stage.

Future work should focus on:

```text
Obsidian Public
       ↓
Information architecture
       ↓
Quartz configuration
       ↓
Website visual design
       ↓
Navigation
       ↓
Research / Notes organization
       ↓
Search
       ↓
Tags / backlinks / graph
       ↓
Custom homepage
       ↓
SEO / metadata
       ↓
Analytics (optional)
```

The important principle is:

> Keep `Public/` as the only publishing boundary.

Everything inside the private Obsidian vault remains private unless explicitly copied or moved into `Public/`.

---

# 32. One-Line Recovery Summary

If the entire website publishing chain needs to be remembered:

```text
Obsidian → Obsidian WebDAV → NAS /CL_base/Public → Quartz timer → /opt/quartz/public → Nginx → www.cltech.xin
```

If Obsidian reports 405 again, first check:

```text
rclone user = obsidian
Public owner = obsidian:obsidian
```

and then check:

```bash
journalctl -u rclone-obsidian.service
```

Do not assume a 405 is an Nginx problem.