Purpose
This project demonstrates how to build a personalized, automated, and secure digital garden pipeline using Quartz 4 hosted on a Virtual Private Server (VPS).
Key Objectives
- Unified Workflow: Write and manage all notes using Obsidian on a local computer.
- Private Synchronization: Use a private GitHub repository as the secure central sync point.
- Selective Publishing: Configure Quartz on the VPS to publish only specific public folders while keeping the rest private.
- Automated Deployment: Implement an automated build and deploy process on the VPS, using Docker and Nginx Proxy Manager to securely serve the site to the world.
This setup achieves a DevOps pipeline that provides full control over data privacy and presentation.
Architecture Diagram
graph TD subgraph Local ["Local Computer (Obsidian Vault)"] A["Obsidian (Markdown Files)"] --> B["Git Client"] end B -->|"SSH Push"| C[("GitHub Private Repo")] subgraph VPS ["VPS (Digital Ocean Droplet)"] C -->|"git pull"| D["Quartz Engine (content/)"] D -->|"npx quartz build"| E["~/quartz-engine/public/"] E -->|"rsync -avz"| F["/var/www/notes/"] subgraph Docker ["Docker Stack"] G["Nginx Proxy Manager"] F -->|"Volume Mount"| H["Quartz Web (Nginx)"] G -->|"Reverse Proxy"| H end end I["User Browser"] --> J["Cloudflare DNS"] J --> G
Steps
1. Local Setup
Before we touch the server, we need the a source of truth
- Install Obsidian
- Create a Vault (needs an
index.mdfile or the build will fail) - Create a private repository on GitHub.
- Open your terminal, go to your Vault folder, and run:
git init
git add .
git commit -m "initial vault"
git remote add origin [your-repo-link]
git push -u origin main2. Preparing the VPS
We need to give the Droplet permission to pull from the private repo.
- Generate a Deployment Key on the VPS:
ssh-keygen -t ed25519 -C "vps-quartz-deploy"
cat ~/.ssh/id_ed25519.pub- Add to GitHub. Copy that output, go to your GitHub Repo → Settings → Deploy Keys → Add deploy key, and paste it there.
3. Server Setup
- Create the directory:
mkdir ~/quartz
cd ~/quartz- Since Quartz produces static files (just HTML/CSS), we just need a tiny web server like Nginx so create a
docker-compose.ymlfile:
services:
quartz:
image: nginx:alpine
container_name: quartz_web
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./public:/usr/share/nginx/html4. Quartz Deployment
Make sure your npm version is up to date with Quartz. If needed, download NVM (Node Version Manager). It allows you to switch versions instantly without breaking your system’s global settings.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash- Now, refresh your terminal so it recognizes the
nvmcommand
source ~/.bashrc
nvm install 22 # or whatever version is needed for your QuartzNow, we can pull the code and set up the filter
- Setup a clone of Quartz:
cd ~
git clone --recursive https://github.com/jackyzha0/quartz.git quartz-engine
cd quartz-engine
npm install- Now, we want to replace the default “content” folder in Quartz with your actual repository
# Remove the demo content
rm -rf content
# Clone your notes repo into a folder named 'content'
# To make use of your auth keys, clone with SSH
# Replace [YOUR_USER] and [YOUR_REPO] with your info
git clone [email protected]:[YOUR_USER]/[YOUR_REPO].git content- We can configure a filter for notes we want to publish by editing the configuration file
quartz.config.ts- Look for configuration → ignorePatterns
- Add the title of any folders you want to ignore
// quartz.config.ts
configuration: {
// ...
ignorePatterns: [
".obsidian",
"private",
"templates",
// add any others you would like to ignore
],
// ...
}5. Build the Site
Now that the filter is set, tell Quartz to transform your Markdown into the static HTML files:
cd ~/quartz-engine
npx quartz build- This creates a
publicfolder inside~/quartz-engine/containing your finished website
6. Create Public Folder
- Nginx (inside the container) will try to read your
publicfolder, but it doesn’t have the clearance to see into your home directory, and you probably don’t want it to - We want to move the
publicfolder to/var/www/notes. - Give ownership to the
www-datagroup - Make the Nginx display
docker-compose.ymlpoint to/var/www/notesso that the home directory remains locked tight while the web server operates in its own designated area.
# Create the directory
sudo mkdir -p /var/www/notes
# Change ownership to your user so you can push files there without sudo
sudo chown -R sydney:sydney /var/www/note7. Connect to Nginx (Display)
Now we need to make sure that public folder is what the world sees.
- Create the directory:
mkdir -p ~/quartz-site
cd ~/quartz-site- Create a
docker-compose.ymlfile:
services:
quartz:
image: nginx:alpine
container_name: quartz_web
restart: unless-stopped
ports:
- "8080:80"
volumes:
- /var/www/notes:/usr/share/nginx/html:ro
command: >
/bin/sh -c "printf 'server {
listen 80;
location / {
root /usr/share/nginx/html;
try_files $$uri $$uri.html $$uri/ =404;
}
}' > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"- Launch it
docker compose up -d8. Setup the Proxy
- Cloudflare
- Add a DNS Record:
- Type:
A - Name:
notes - IPv4 Address:
<droplet ipv4 address> DNS Onlyproxy status to allow non-standard port traffic
- Type:
- Add a DNS Record:
- NPM Dashboard
- Add a Proxy Host:
- Domain Name:
notes.sydneysu.dev - Scheme:
http - Forward IP:
137.184.191.136\ - Forward Port:
8080 - Block common Exploits
- SSL: Request new Let’s Encrypt certificate + Force SSL
- Domain Name:
- Add a Proxy Host:
9. Update Script
- Let’s create a small bash script
update-notes.shto automate the update process
#!/bin/bash
# 1. Go to your engine folder and pull the raw Markdown
echo "--- Pulling latest notes from GitHub ---"
cd ~/quartz-engine/content
git pull
# 2. Rebuild the static site
echo "--- Rebuilding Quartz Garden ---"
cd ~/quartz-engine
npx quartz build
# 3. Sync only the PUBLIC build to the system web folder
echo "--- Deploying to /var/www/notes ---"
rsync -avz --delete ~/quartz-engine/public/ /var/www/notes/
echo "--- Update Complete! ---"- Make it executable
chmod +x ~/update-notes.sh10. Automation Alias
- Add an alias for
update-notes.shin~/.bashrc:
alias garden='~/update-notes.sh'- Refresh the config
source ~/.bashrc- Run it
garden