Setting Up Joplin Server on Raspberry Pi
Why self-host Joplin?#
Joplin Cloud costs money, and I don’t love my notes sitting on someone else’s server. The Pi was already running 24/7 anyway, so spinning up a sync server was a natural fit. Total cost: ₹0/month.
The alternatives I considered:
| Option | Why I skipped it |
|---|---|
| Joplin Cloud | Paid, data on third-party servers |
| Nextcloud sync | Too heavy for a 2GB Pi just for notes |
| Dropbox/OneDrive | Works but sync conflicts are painful |
| Self-hosted Joplin Server | ✅ Free, fast, full control |
Stack#
- Joplin Server — the official sync backend (Docker)
- PostgreSQL 15 — note storage and metadata (Docker)
- nginx — reverse proxy with SSL termination
- Bash script — automated daily
pg_dumpbackups - Tailscale — remote access from anywhere
Prerequisites#
- Raspberry Pi 4B running headless Raspberry Pi OS (64-bit)
- Docker and Docker Compose installed
- A domain or local hostname for nginx (e.g.,
joplin.home.lan)
Step 1 — Docker Compose setup#
Create a working directory:
mkdir -p ~/docker/joplin && cd ~/docker/joplin
Create the compose.yaml:
services:
joplin:
image: joplin/server:latest
container_name: joplin-server
restart: unless-stopped
ports:
- "22300:22300"
environment:
- APP_BASE_URL=https://joplin.yourdomain.com
- APP_PORT=22300
- DB_CLIENT=pg
- POSTGRES_HOST=db
- POSTGRES_PORT=5432
- POSTGRES_DATABASE=joplin
- POSTGRES_USER=joplin
- POSTGRES_PASSWORD=changeme
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
container_name: joplin-db
restart: unless-stopped
environment:
- POSTGRES_DB=joplin
- POSTGRES_USER=joplin
- POSTGRES_PASSWORD=changeme
volumes:
- ./pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U joplin"]
interval: 10s
timeout: 5s
retries: 5
Bring it up:
docker compose up -d
Note: The healthcheck on the db service ensures Joplin Server doesn’t start until PostgreSQL is actually ready to accept connections — avoids the classic “connection refused on first boot” issue.
Step 2 — nginx reverse proxy#
server {
listen 80;
server_name joplin.yourdomain.com;
client_max_body_size 200M; # allow large note attachments
location / {
proxy_pass http://localhost:22300;
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;
}
}
Key detail: the client_max_body_size 200M is important — without it, nginx will reject note attachments (PDFs, images) larger than the default 1MB.
Step 3 — Automated daily backups#
The database holds everything — notes, notebooks, tags, attachments. Losing it would be painful. A simple pg_dump cron job handles this:
#!/bin/bash
# /home/pi/scripts/backup-joplin.sh
BACKUP_DIR="/mnt/ssd/backups/joplin"
DATE=$(date +%Y-%m-%d)
mkdir -p "$BACKUP_DIR"
# Dump the database
docker exec joplin-db pg_dump -U joplin joplin \
> "$BACKUP_DIR/joplin-$DATE.sql"
# Keep only last 7 days
find "$BACKUP_DIR" -name "*.sql" -mtime +7 -delete
echo "[$(date)] Joplin backup completed: joplin-$DATE.sql"
Make it executable and add to crontab:
chmod +x /home/pi/scripts/backup-joplin.sh
# crontab -e
0 2 * * * /home/pi/scripts/backup-joplin.sh >> /var/log/joplin-backup.log 2>&1
Backups run at 2 AM daily, old dumps auto-deleted after 7 days. The SSD has plenty of room — each daily dump is only a few MB.
Step 4 — Connect the Joplin app#
On every device (phone, laptop, desktop):
- Open Joplin → Settings → Synchronisation
- Set sync target to Joplin Server
- Enter the server URL:
https://joplin.yourdomain.com - Log in with default credentials (
admin@localhost/admin) and change the password immediately
Sync happens automatically in the background. Notes, notebooks, tags, and attachments all stay in sync across every device.
Gotchas I hit along the way#
- First-time login: The default admin credentials are
admin@localhost/admin— easy to miss in the docs. Change them right after first login. - ARM image support: The official
joplin/serverimage supports ARM64, so it runs natively on the Pi without emulation. No performance issues. - Database migrations: On version upgrades, Joplin Server runs migrations automatically on startup. Just pull the new image and
docker compose up -d. - Disk space: PostgreSQL data lives on the SSD (
./pgdata). Keep an eye on it if you store lots of large attachments.
Result#
Notes sync instantly across all devices — phone, laptop, desktop. Backups run at 2 AM daily and I sleep fine. The whole thing uses barely any resources on the Pi:
$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
NAME CPU % MEM USAGE / LIMIT
joplin-server 0.15% 85MiB / 1.8GiB
joplin-db 0.05% 35MiB / 1.8GiB
Total memory footprint: ~120MB. Perfectly fine on the 2GB Pi.
Joplin is running as a service on the homelab → view service details