A lightweight, high-performance CI/CD webhook server written in Racket.
This project provides a professional self-hosting alternative for Obsidian Digital Garden users. It allows you to transition from Vercel to your own VPS, automating the deployment of your notes while keeping your infrastructure private and efficient.
- ๐ Self-Hosted Freedom: Full control over your deployment pipeline
- ๐ Designed for Obsidian: Optimized for the "Obsidian Digital Garden" plugin workflow
- โก Asynchronous Builds: Responds to GitHub immediately while processing builds in background
- ๐ Concurrency Safety: Semaphore-based locking prevents simultaneous builds
- ๐ Security: HMAC-SHA256 signature verification for GitHub webhooks
- ๐ Flexible Deployment: Direct HTTP or Nginx reverse proxy modes
- ๐ฆ Remote Deploy: Optional rsync-based deployment to separate web servers
- ๐ Health Monitoring: Built-in
/healthendpoint for status checks - ๐ Educational: A practical example of Racket's multithreading capabilities
GitHub โ HTTP Webhook โ Racket (0.0.0.0:8080) โ Build โ Deploy
Pros: Simple setup, no Nginx required
Cons: No HTTPS, must disable SSL verification in GitHub
GitHub โ HTTPS Webhook โ Nginx (443/8443) โ Racket (127.0.0.1:8080) โ Build โ Deploy
Pros: Secure HTTPS, SSL verification enabled
Cons: Requires Nginx and SSL certificate setup
Build Server:
GitHub โ Webhook โ Racket โ Build โ rsync
Web Server:
Nginx โ Static Files (synced via rsync)
Pros: Separation of concerns, scalable, secure
Cons: Requires two servers and SSH key setup
- Racket (v8.0+)
- Node.js & npm (v22+ recommended)
- OpenSSL
- Git
- rsync (if using remote deploy)
- Nginx (optional, for HTTPS)
- Nginx
- rsync
git clone https://github.com/yourusername/racket-deployer.git
cd racket-deployercp config.example.json config.json
nano config.jsonMinimal Configuration (Direct HTTP):
{
"github-secret": "your-strong-secret",
"port": 8080,
"listen-ip": "0.0.0.0",
"repo-path": "/var/www/blog",
"repo-url": "https://github.com/username/repo.git",
"build-output": "/var/www/blog/dist"
}Production Configuration (Nginx + Remote Deploy):
{
"github-secret": "your-strong-secret",
"port": 8080,
"listen-ip": "127.0.0.1",
"repo-path": "/var/www/blog",
"repo-url": "https://github.com/username/repo.git",
"build-output": "/var/www/blog/dist",
"deploy": {
"enabled": true,
"remote-host": "user@web-server-ip",
"remote-path": "/var/www/blog/dist",
"ssh-key": "/home/user/.ssh/id_rsa",
"rsync-options": "-avz --delete"
}
}sudo mkdir -p /var/www/blog
sudo chown -R $USER:$USER /var/www/blog
git clone https://github.com/username/your-blog.git /var/www/blog
cd /var/www/blog
npm install
npm run build # Test buildcd racket-deployer
racket main.rktFor Direct HTTP Mode:
- Payload URL:
http://your-server-ip:8080/ - Content type:
application/json - Secret: Your
github-secret - SSL verification: โ Disable
For Nginx HTTPS Mode:
- Payload URL:
https://webhook.your-domain.com:8443/ - Content type:
application/json - Secret: Your
github-secret - SSL verification: โ Enable
| Option | Description | Default | Required |
|---|---|---|---|
github-secret |
GitHub webhook secret | - | Yes |
port |
Server port | 8080 | Yes |
listen-ip |
Listen address (0.0.0.0 or 127.0.0.1) |
127.0.0.1 |
Yes |
repo-path |
Local repository path | - | Yes |
repo-url |
GitHub repository URL | - | Yes |
build-output |
Build output directory | - | Yes |
deploy.enabled |
Enable remote deployment | false |
No |
deploy.remote-host |
Remote server (user@host) | - | If deploy enabled |
deploy.remote-path |
Remote directory path | - | If deploy enabled |
deploy.ssh-key |
SSH private key path | - | If deploy enabled |
deploy.rsync-options |
rsync command options | -avz --delete |
No |
| Value | Description | Use Case |
|---|---|---|
0.0.0.0 |
Listen on all interfaces | Direct HTTP access, testing, internal networks |
127.0.0.1 |
Listen on localhost only | Production with Nginx reverse proxy |
server {
listen 8443 ssl http2;
server_name webhook.your-domain.com;
ssl_certificate /etc/letsencrypt/live/webhook.your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/webhook.your-domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://127.0.0.1:8080;
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}Enable and test:
sudo ln -s /etc/nginx/sites-available/webhook /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxsudo apt install certbot python3-certbot-nginx
sudo certbot certonly --nginx -d webhook.your-domain.comssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
cat ~/.ssh/id_rsa.pub# On web server
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Paste the public key, save
chmod 600 ~/.ssh/authorized_keys# On build server
ssh -i ~/.ssh/id_rsa user@web-server-ip "echo 'SSH OK'"rsync -avz --delete -e 'ssh -i ~/.ssh/id_rsa' \
/var/www/blog/dist/ \
user@web-server-ip:/var/www/blog/dist \
--dry-runsudo nano /etc/systemd/system/blog-deploy.service[Unit]
Description=Blog Deploy Webhook Server
After=network.target
[Service]
Type=simple
User=youruser
Group=youruser
WorkingDirectory=/home/youruser/racket-deployer
Environment="PATH=/usr/bin:/bin:/usr/local/bin"
ExecStart=/usr/bin/racket /home/youruser/racket-deployer/main.rkt
Restart=always
RestartSec=10
StandardOutput=append:/var/log/blog-deploy.log
StandardError=append:/var/log/blog-deploy-error.log
[Install]
WantedBy=multi-user.targetStart service:
sudo systemctl daemon-reload
sudo systemctl enable blog-deploy
sudo systemctl start blog-deploy
sudo systemctl status blog-deploy| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Service status |
/health |
GET | Build status and last build time |
/ |
POST | GitHub webhook receiver |
# Basic status
curl http://localhost:8080
# Response:
# Blog Deploy Webhook
# Status: Running
# Health check
curl http://localhost:8080/health
# Response:
# Status: idle
# (or: Status: building / success / failed)
# Last build: 120 seconds ago# Check logs
sudo journalctl -u blog-deploy -n 50
# Test manually
cd ~/racket-deployer
racket main.rkt# Check GitHub webhook deliveries
# Repository โ Settings โ Webhooks โ Recent Deliveries
# Check signature
sudo journalctl -u blog-deploy -f
# Look for: โ Signature verified# Manual build
cd /var/www/blog
npm run build
# Check disk space
df -h
# Check Node.js version
node --version# Test SSH
ssh -i ~/.ssh/id_rsa user@web-server "echo OK"
# Test rsync manually
rsync -avz -e 'ssh -i ~/.ssh/id_rsa' \
/var/www/blog/dist/ \
user@web-server:/var/www/blog/dist
# Check SSH key permissions
ls -la ~/.ssh/id_rsa # Should be -rw-------
chmod 600 ~/.ssh/id_rsaIf using HTTP proxy, disable it for localhost:
export no_proxy="localhost,127.0.0.1"- Strong Secrets: Generate with
openssl rand -hex 32 - SSH Keys: Use dedicated keys with proper permissions (600)
- Firewall: Only open necessary ports
- Nginx: Use HTTPS in production
- Updates: Keep dependencies updated
Direct HTTP Mode:
- Open: 8080 (webhook), 22 (SSH)
Nginx Proxy Mode:
- Open: 8443 (webhook HTTPS), 22 (SSH)
- Closed: 8080 (Racket, local only)
| Feature | Direct HTTP | Nginx Proxy | Separated Deploy |
|---|---|---|---|
| Setup Complexity | โญ Simple | โญโญ Medium | โญโญโญ Complex |
| HTTPS Support | โ | โ | โ |
| Security | โ Good | โ Excellent | |
| SSL Verification | โ | โ | โ |
| Scalability | โญ | โญโญ | โญโญโญ |
| Recommended For | Testing | Small sites | Production |
Contributions welcome! Please feel free to submit a Pull Request.
MIT License - free to use for personal or commercial purposes.
- Built with Racket
- Inspired by Obsidian Digital Garden
- Designed as a Vercel alternative for self-hosting enthusiasts
- ๐ Issues: GitHub Issues
- ๐ฌ Discussions: GitHub Discussions
Happy deploying! ๐