Self-hosted likes for Ghost members, backed by SQLite and delivered as a Node service.
git clone https://github.com/ifrederico/ghost-like-button.git
cd ghost-like-button
bash install.shThe installer will:
- Prompt for your Ghost URL (full URL with https://)
- Auto-detect your Ghost Docker network
- Create necessary directories with correct permissions
- Start the service
If you prefer to configure everything yourself:
# Clone the repository
git clone https://github.com/ifrederico/ghost-like-button.git
cd ghost-like-button
# Create and configure .env
cp .env.example .env
nano .env # Set your GHOST_URL
# Find your Ghost Docker network
docker network ls | grep ghost
# Edit compose.yml with your network name
nano compose.yml # Replace ghost_ghost_network with your actual network
# Create data directory with correct permissions
mkdir -p data
chown -R 1000:1000 data/
# Start the service
docker compose up -d- Docker + Docker Compose
- A running Ghost instance (the like service joins its Docker network)
- Ghost members enabled (Settings → Membership)
Looking for a complete example? Check out Pastiche - a Ghost theme with the like button already integrated.
Add the like button to your Ghost theme:
In post.hbs or index.hbs:
Create assets/js/ghost-like-button.js:
customElements.define('like-button', class extends HTMLElement {
async connectedCallback() {
const base = this.getAttribute('api') ?? '/ghost-like-button';
const target = encodeURIComponent(this.getAttribute('url'));
const btn = Object.assign(document.createElement('button'), { textContent: '❤ 0' });
btn.addEventListener('click', () => this.toggle(`${base}/update-likes?url=${target}`, btn));
this.append(btn);
this.refresh(`${base}/get-likes?url=${target}`, btn);
}
async refresh(endpoint, btn) {
const res = await fetch(endpoint, { credentials: 'include' });
if (res.ok) btn.textContent = `❤ ${await res.text()}`;
}
async toggle(endpoint, btn) {
const res = await fetch(endpoint, { method: 'POST', credentials: 'include' });
if (res.ok) btn.textContent = `❤ ${await res.text()}`;
}
});Add to your Caddy configuration (see Caddyfile.example for full examples):
yourdomain.com {
# Your existing Ghost config...
handle_path /ghost-like-button/* {
reverse_proxy ghost-like-button:8787
}
# Your Ghost service
reverse_proxy ghost:2368
}| Variable | Required | Default | Purpose |
|---|---|---|---|
GHOST_URL |
Yes | - | Full Ghost URL (e.g., https://yourdomain.com). Used for CORS and JWT validation. |
PORT |
No | 8787 |
Internal HTTP port. |
NODE_ENV |
No | production |
Standard Node environment flag. |
Edit .env to change these values.
| Method & Path | Description |
|---|---|
GET /health |
Basic health probe. |
GET /get-likes?url=<post-url> |
Returns total likes (text body). |
POST /update-likes?url=<post-url> |
Toggles like/unlike for the member. |
GET responses include the X-Has-Liked header (1 or 0) when a member JWT is present.
cd ghost-like-button
git pull
docker compose pull
docker compose up -dYour database in ./data/ is preserved across updates.
# View logs
docker compose logs ghost-like-button
# Check database
sqlite3 data/ghost-like-button.db ".tables"
# Compact database (optional)
sqlite3 data/ghost-like-button.db "PRAGMA wal_checkpoint(TRUNCATE);"
# Restart service
docker compose restart ghost-like-buttonYour SQLite database is stored in ./data/ghost-like-button.db. Back it up like any other file:
cp data/ghost-like-button.db data/ghost-like-button.db.backup- Rate limiting: 90 requests/minute per IP
- CORS restricted to your Ghost domain
- JWT validation (trusted internal network)
- No personal data stored except email for deduplication
MIT License - See LICENSE file
Open an issue on GitHub if you need help with setup.
