Old: https://vttc08.github.io/obsidian-notes
New: https://vttc08.github.io/obsidian-notes-v2
Repo: https://github.com/vttc08/obsidian-notes-v2
Also on Cloudflare Pages
VPS Deployment

Current Issues
My previous obsidian-publish implementation has several limitations that need addressing:
Publishing Constraints
- Single-device dependency: Publishing only works from my desktop with specific Windows/PowerShell setup
- Complex environment requirements: Requires NodeJS, Quartz, correct Git repo location, and SMB share access, which is complex to setup on new devices
- Platform limitations: No support for non-Windows devices, mobile phones, or shared computers
Technical Issues
- File handling: Copy scripts don’t handle file deletions properly
- No live preview: Local server serves as the only preview method
- Date metadata loss: GitHub Pages builds lose filesystem dates since I don’t use frontmatter, causing incorrect “recent notes” ordering
- Fragile workflow: Entire process breaks if desktop is unavailable or environment changes
These limitations make note publishing inflexible and unreliable, especially when away from my primary desktop setup.
Proposed Plans
To address these limitations, I’m rebuilding the publishing workflow with server-side processing using Docker and SSH for universal access.
Core Architecture:
- Server-side publishing: All build processes run on a dedicated Linux server with Docker
- Universal access: Any device with SSH or web browser can trigger publishing
- Docker containerization: Handles NodeJS/Quartz environment without local dependencies
Access Methods:
- SSH command: Simple one-liner from any terminal:
ssh server "deploy.sh" - Obsidian Shell: Direct integration for desktop publishing
- Web-based terminal: Using webtop Docker container for browser-based access
- Mobile SSH: Terminal apps for mobile publishing
Key Benefits:
- Device independence: No platform-specific requirements
- Always available: Server runs 24/7, no desktop dependency
- Simplified setup: Single server configuration vs. per-device setup
- Reliable workflow: Containerized environment prevents configuration drift
The webtop solution provides a full Obsidian environment in browsers, though mobile KasmVNC is unusable, but SSH terminal access works fine.

Quartz
The setup and customization of Quartz has been the same, here are the summary.
Installation
Install NodeJS and Quartz. I opted to install it in WSL rather than Windows itself to mimic a Linux environment. The instructions for starting with Quartz.
Changes
Added my own list of notes to ignore. There are 2 ignore types in Quartz, and adding notes to both is nessecary
.gitignore: repository, it will prevent the file from tracked by gitquartz.config.ts: quartz, quartz will not build the website for these notes
- by default, even using
HardLineBreak, there are some issues with how Obsidian and Quartz looks with line breaks after a markdown list.

Display Recent Notes on Home Page
- I used CSS flex properties to have the homepage display a responsive layout with multiple columns

Deployment Issues
I used the official recommended method to publish to Github pages, with a custom Github action that copies built HTML to my VPS for CI/CD demonstration. For home server deployment, I copy build output to an Nginx folder integrated with my existing reverse proxy.
Date Issue
Recent notes display randomly on Github Pages and VPS, but correctly on my home server. Since I don’t use frontmatter dates, Quartz relies on filesystem timestamps, which are lost when committing to Github, everything inherits the commit date instead of original modification time.
Rather than retrofitting frontmatter dates across all notes, I solved this using ghp-import to build locally with preserved filesystem dates, then push static HTML to the gh-pages branch for both Github and Cloudflare Pages to serve.
Docker Issue
Testing the official Quartz image, I came across several challenges. Using user: ${PUID}:${PGID} for proper file ownership caused errors, requiring this patch.
Yet this only worked on WSL, because my bind mount consists of node_modules. On my Linux server, bind mounting overwrote the container’s node_modules with my empty host directory, breaking dependencies. Without bind mounting, the user directive failed as source code became root-owned. The solution: run as root and use chown -R $PUID:$PGID on the public folder afterward.
What Didn’t Work
The webtop image came with Docker command preinstalled, I thought I could run my Docker commands directly. By default, the Docker socket is not expose to the container, but using docker-socket-proxy, I was able to safely expose Docker to webtop. However, Docker parses compose file locally, relative paths such as ./ or ~/ would resolve to the path in the container, rather than the host.

My Architecture
So what is the architecture that balances all the compromises. Here is the tree structure.
├── obsidian-notes-v2/
│ ├── compose.yml
│ ├── deploy.sh
│ ├── dev.sh
│ ├── Dockerfile
│ ├── content/
│ ├── public/
│ ├── quartz/
│ │ ├── <project_source_code>
│ ├── quartz.config.ts
│ ├── quartz.layout.ts
├── obsidian-webtop/
│ ├── compose.yml
│ ├── deploy
│ ├── dev
│ ├── Documents/
│ │ └── ssh/
│ │ └── openssh_keys/
└── quartz-web/
├── site/
├── compose.yml
└── nginx.conf
In the master obsidian folder, I have 3 folder
obsidian-note-v2- git repo where it keep tracks of source code related changes, as well as the working directory for deployment related tasksobsidian-webtop- project folder for Docker obsidian webtopquartz-web- Nginx docker container for serving static files
There are few possible ways to trigger deployment
- by using obsidian-shell
- using webtop in a web browser, opening terminal and type
deploy - manually SSH into the server and running
deploy.shlocated inobsidian-notes-v2
In all cases, the deploy.sh is the entrypoint for everything. Here are the steps it occurs when deploy.sh is triggered
- change into
obsidian-notes-v2since all Docker and git repo related files are here
cd $(dirname ${BASH_SOURCE[0]})- run rsync command to copy my notes from my vault (also saved in Linux server) to
content/, this step is needed so I can keep track of it using git, it deletes any files that was deleted in the vault
rsync -ahP --delete ~/Documents/notes/ ./content/- use Docker to run an ephemeral container and build the site using quartz, the container uses
content/folder consisting of markdown files and generated static files inpublic/
docker compose run --rm quartz-builder- run
ghp-importwhich automatically commit changes inpublic/intogh-pages
uv un ghp-import public/ -p- synchronize the files from
public/into production Nginx folder
rsync -ahP --delete --remove-source-files ./public/ ../quartz-web/site/- git related tasks such as add, commit and push
Docker Compose
The compose file does the heavy lifting when building the site. It consists of 3 sections
quartz-service- general environment and information about the quartz containerquartz-builder- building the HTML from markdownquartz-dev- development server
The compose file uses environment variables QUARTZ_OUTPUT_DIR|CONTENT_DIR so the npx command can be consistent, as well as PUID/PGID so the chown can ensure the emitted file are correct permission despite the container is running as root. The standardized configuration allows for flexibility, I simply have to change the bind mount, add optional read-only and the build will run stateless-ly.
Dev Server
To sort out live preview, I’ve also added a dev server. It uses --serve to live preview the site. However, Websocket doesn’t work, even though it works outside of Docker. To run it ephemerally as well as using port forwarding, this command is needed.
docker compose run --rm --name quartz-dev --service-ports quartz-devTo add convenience into the webtop container. I’ve created deploy and dev in obsidian-webtop/ which are simply wrappers for deploy.sh and dev.sh and SSH, then I used bind mount to ./deploy:/config/.local/bin, so these scripts are in the container’s shell PATH, and I can simply type deploy to execute the desired script over SSH. Similarly, in obsidian-shell, the command for deployment is much easier. Here’s the original Powershell script
sleep 0.5
$dg = [Environment]::GetFolderPath("MyDocuments") + "\Projects\obsidian-publish"
function cpy{
param ($src, $dest, $opt)
robocopy $src $dest /E /NDL /NJH /XF *.py *.ipynb $opt
}
cpy . "$dg\content"
cd $dg
npx quartz build
cpy public/ "\\10.10.120.12\docker\quartz-web\site" /NFL
if (${{_github}}) {
npx quartz sync -m "${{_commit}}"
echo "Published to github."
} else { echo "Process is done." }And the updated script, which works on any computer
ssh -t laptopserver "~/docker/obsidian/obsidian-notes-v2/deploy.sh"Here is a diagram showing the workflow, which may be easier to understand.

