Joplin Server Sync: Sync Your End-to-End Encrypted Notes via Docker
Set up a dedicated Joplin Server using Docker Compose. Sync notes, tasks, and media securely across all your devices with full control.
Self-hosting Joplin Server provides a private, high-performance synchronization backend for Joplin notes. Compared to WebDAV or Dropbox, the dedicated Joplin Server is significantly faster, supports multiple users, and handles concurrent syncing of large attachments more efficiently.
Prerequisites and Directory Structure
This deployment requires a VPS running Docker (with Docker Compose v2) and a domain or subdomain configured with an A record pointing to the server's IP address.
Create the directory structure to isolate configuration files and database volumes:
mkdir -p /opt/joplin-server/{app,db}
cd /opt/joplin-server
Environment Configuration
Separate secrets and environment variables from the Docker Compose file using a .env file. Create /opt/joplin-server/.env with the following configuration:
# --- General Settings ---
JOPLIN_DOMAIN=joplin.example.com
JOPLIN_PORT=22300
# --- PostgreSQL Config ---
POSTGRES_USER=joplin_admin
POSTGRES_PASSWORD=secure_random_db_password_here
POSTGRES_DB=joplin_db
# --- Joplin Server Config ---
# The base URL must match the external domain used by clients, including protocol
APP_BASE_URL=https://joplin.example.com
APP_PORT=22300
# --- Security & Mail (Optional but recommended for user administration) ---
# MAILER_ENABLED=true
# MAILER_HOST=smtp.mailgun.org
# MAILER_PORT=587
# MAILER_SECURITY=tls
# MAILER_USER=postmaster@example.com
# MAILER_PASSWORD=smtp_password
# MAILER_NOREPLY=noreply@example.com
Docker Compose Configuration
Create /opt/joplin-server/docker-compose.yml to define the multi-container setup containing Joplin Server and a PostgreSQL database.
version: '3.8'
services:
db:
image: postgres:16-alpine
container_name: joplin_db
restart: unless-stopped
security_opt:
- no-new-privileges:true
volumes:
- ./db:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1024M
cpus: '1.0'
app:
image: joplin/server:latest
container_name: joplin_app
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "127.0.0.1:${JOPLIN_PORT}:22300"
depends_on:
db:
condition: service_healthy
environment:
- APP_PORT=${APP_PORT}
- APP_BASE_URL=${APP_BASE_URL}
- DB_CLIENT=pg
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DATABASE=${POSTGRES_DB}
- POSTGRES_HOST=db
- POSTGRES_PORT=5432
# - MAILER_ENABLED=${MAILER_ENABLED}
# - MAILER_HOST=${MAILER_HOST}
# - MAILER_PORT=${MAILER_PORT}
# - MAILER_SECURITY=${MAILER_SECURITY}
# - MAILER_authUser=${MAILER_USER}
# - MAILER_authPassword=${MAILER_PASSWORD}
# - MAILER_NOREPLY=${MAILER_NOREPLY}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:22300/ping"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 1024M
cpus: '1.0'
Deploy the stack with:
docker compose up -d
Reverse Proxy Configuration (Nginx)
Joplin Server should not be exposed directly to the public internet. Secure it behind Nginx with Let's Encrypt SSL/TLS certificates.
Nginx Server Block
Create a configuration file (e.g., /etc/nginx/sites-available/joplin.conf):
server {
listen 80;
listen [::]:80;
server_name joplin.example.com;
# Redirect all HTTP traffic to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name joplin.example.com;
# SSL Certificates (Managed by Certbot)
ssl_certificate /etc/letsencrypt/live/joplin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/joplin.example.com/privkey.pem;
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security Headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Connection and upload size limits
client_max_body_size 400M; # Crucial for large attachments/media sync
location / {
proxy_pass http://127.0.0.1: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;
# Disable buffering to prevent sync request delays
proxy_buffering off;
proxy_request_buffering off;
}
}
Enable the configuration and reload Nginx:
ln -s /etc/nginx/sites-available/joplin.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
First-Time Initialization
- Navigate to your configured domain (e.g.,
https://joplin.example.com). - Log in using the default credentials:
- Email:
admin@localhost - Password:
admin - Change the password immediately under the My Profile tab.
- Set up non-admin users in the Users menu if you intend to share the instance.
Client Sync Configuration
To connect the desktop or mobile Joplin applications to your self-hosted Joplin Server:
- Open the Joplin application.
- Navigate to Tools > Options (Desktop) or Configuration (Mobile).
- Select the Sync tab.
- Set Synchronisation target to Joplin Server (Beta / Official).
- Configure the following parameters:
- Joplin Server URL:
https://joplin.example.com(Must match theAPP_BASE_URLexactly, without trailing slashes). - Username (Email): The email configured for your user (e.g.,
admin@localhostor your custom email). - Password: The password set during initialization.
- Click Check synchronisation configuration to verify.
Enabling End-to-End Encryption (E2EE)
Joplin Server acts as a zero-knowledge store when End-to-End Encryption is active. This ensures data is encrypted locally on your client device before transmission to the server.
- In the Joplin Client, go to Options > Encryption.
- Click Enable Encryption.
- Choose a master password. Store this password in a secure password manager. Losing this password means losing access to your note data.
- The client will generate key pairs and encrypt the local database. Subsequent synchronizations will push only encrypted payloads to the PostgreSQL database.
Automated Database Backups
Because Joplin Server uses a relational database, standard file backups are insufficient. Set up a cron task to perform atomic PostgreSQL dumps.
Create a backup script at /opt/joplin-server/backup.sh:
#!/bin/bash
BACKUP_DIR="/opt/joplin-server/backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# Run pg_dump inside the docker container
docker exec joplin_db pg_dump -U joplin_admin -d joplin_db -F c -b -v -f "/var/lib/postgresql/data/backup_${DATE}.sql"
# Move the backup file out of the container volume to the backup directory
mv /opt/joplin-server/db/backup_${DATE}.sql "$BACKUP_DIR/"
# Retain only the last 7 days of backups
find "$BACKUP_DIR" -type f -name "*.sql" -mtime +7 -delete
Make it executable:
chmod +x /opt/joplin-server/backup.sh
Add a daily Cron job by running crontab -e:
0 2 * * * /opt/joplin-server/backup.sh >/dev/null 2>&1