Self-Host Joplin Server
Self-host Joplin Server with Docker Compose, PostgreSQL, Nginx TLS reverse proxy, and end-to-end encryption for private, cross-device note sync.
Before you start
- ▸A Linux server with a domain name whose A record resolves to its public IP
- ▸Docker Engine 24+ and Docker Compose v2 installed
- ▸Ports 80 and 443 reachable from the internet
- ▸Root or sudo access on the server
Joplin Server lets you sync notes across all your devices without trusting a third-party cloud. You run the server, you own the data. This guide walks through a production-ready self-hosted setup: Joplin Server in Docker Compose, a PostgreSQL backend, an Nginx reverse proxy with TLS, and end-to-end encryption configured on the client side.
Prerequisites and Architecture
You need a Linux server with a public domain name pointed at it (A record resolves correctly), Docker Engine 24+ and Docker Compose v2 installed, and ports 80 and 443 open through your firewall. The stack is: PostgreSQL 15 as the database, Joplin Server (the official image from joplin/server), and Nginx as a TLS-terminating reverse proxy managed by Certbot.
Step 1 — Prepare the Directory and Environment File
Create a dedicated directory and a .env file to hold secrets outside of your Compose file.
mkdir -p /opt/joplin && cd /opt/joplin
cat > .env <<'EOF'
POSTGRES_USER=joplin
POSTGRES_PASSWORD=changeme_strong_password
POSTGRES_DB=joplin
APP_BASE_URL=https://notes.example.com
APP_PORT=22300
EOF
chmod 600 .env
Replace changeme_strong_password with a randomly generated value (openssl rand -base64 32) and set APP_BASE_URL to your real domain. Never commit .env to version control.
Step 2 — Write the Docker Compose File
Create docker-compose.yml in /opt/joplin:
cat > docker-compose.yml <<'EOF'
services:
db:
image: postgres:15-alpine
restart: unless-stopped
env_file: .env
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
joplin:
image: joplin/server:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
env_file: .env
environment:
- DB_CLIENT=pg
- POSTGRES_HOST=db
- POSTGRES_PORT=5432
ports:
- "127.0.0.1:22300:22300"
volumes:
db_data:
EOF
Binding Joplin's port to 127.0.0.1 means it is only reachable from localhost — Nginx will proxy inbound HTTPS traffic to it. Do not expose port 22300 publicly.
Step 3 — Start the Stack
docker compose up -d
docker compose logs -f joplin
Wait until you see a line indicating the server is listening. Joplin Server runs database migrations automatically on first start, which takes a few seconds. The default admin credentials are admin@localhost / admin — you will change these immediately after TLS is configured.
Step 4 — Install Nginx and Certbot
Choose the block matching your distribution.
Debian / Ubuntu
apt update && apt install -y nginx certbot python3-certbot-nginx
Fedora / RHEL 9 / Rocky Linux 9
dnf install -y nginx certbot python3-certbot-nginx
systemctl enable --now nginx
Arch Linux
pacman -S --noconfirm nginx certbot certbot-nginx
systemctl enable --now nginx
Step 5 — Configure the Nginx Reverse Proxy
Create a server block for your domain. Replace notes.example.com throughout.
cat > /etc/nginx/sites-available/joplin.conf <<'EOF'
server {
listen 80;
server_name notes.example.com;
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;
# Joplin sync can upload large attachments
client_max_body_size 100M;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
EOF
ln -s /etc/nginx/sites-available/joplin.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
On Fedora/RHEL/Arch, place the file in /etc/nginx/conf.d/joplin.conf instead of using sites-available; the symlink step is not needed.
Step 6 — Obtain a TLS Certificate
certbot --nginx -d notes.example.com --agree-tos -m [email protected] --redirect
Certbot rewrites the Nginx config to add an HTTPS server block and a redirect from port 80. It also installs a systemd timer (certbot.timer) to handle automatic renewal — verify it with:
systemctl status certbot.timer
Step 7 — Open the Firewall
ufw (Ubuntu / Debian)
ufw allow 'Nginx Full'
ufw reload
firewalld (Fedora / RHEL / Rocky)
firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --reload
Step 8 — Secure the Admin Account
Open https://notes.example.com in a browser. Log in with admin@localhost / admin. Immediately navigate to Admin → Users → admin@localhost and change the email to a real address and set a strong password. If you are the only user, you can disable new user registration under Admin → Settings.
Step 9 — Connect Joplin Clients
In the Joplin desktop or mobile app go to Tools → Options → Synchronisation (desktop) or Configuration → Sync (mobile). Set:
- Sync target: Joplin Server
- Joplin Server URL:
https://notes.example.com - Email / Password: your admin (or a per-user) account
Click Check synchronisation configuration — a green success message confirms the client can reach the server.
Step 10 — Enable End-to-End Encryption
E2EE encrypts note content on the device before it is uploaded; even the server operator cannot read notes. Configure it on each client independently.
- Go to Tools → Options → Encryption (desktop) or Configuration → Encryption (mobile).
- Click Enable encryption and set a strong master password. Store this password somewhere safe — it cannot be recovered.
- On subsequent devices, sync first so they download the encrypted master key, then enter the same master password when prompted.
E2EE uses AES-256 to encrypt note bodies and attachments. Note titles and metadata (notebook names, timestamps) are also encrypted as of Joplin 2.x. The server stores only opaque ciphertext.
Verification
Run a quick end-to-end check:
# Confirm Joplin Server responds through the proxy
curl -s -o /dev/null -w "%{http_code}" https://notes.example.com/api/ping
A healthy server returns 200 and the body {"status":"ok","version":".... Also confirm the containers are healthy:
docker compose ps
Both db and joplin should show Up with the db reporting (healthy).
Keeping the Stack Updated
Joplin Server follows Joplin's release cadence. Update by pulling the new image and recreating the container — migrations run automatically:
cd /opt/joplin
docker compose pull
docker compose up -d
Pin to a specific version tag (e.g. joplin/server:2.14) in production if you want controlled upgrades rather than tracking latest.
Troubleshooting
- 502 Bad Gateway: Joplin container is not running or hasn't finished migrating. Check
docker compose logs joplin. The port binding must be127.0.0.1:22300:22300, not0.0.0.0. - Database connection refused: The
depends_onhealthcheck should prevent this, but if Postgres takes too long on slow storage, increase the healthcheckretriesvalue. - Certificate not renewing: Run
certbot renew --dry-run. Ensure port 80 is open; Certbot uses HTTP-01 challenge by default. - Sync fails with 413: Increase
client_max_body_sizein Nginx and reload. Joplin's default attachment limit is also configurable via theMAX_TIME_DRIFTand related env vars in the server container. - Lost E2EE master password: There is no recovery path. You must disable encryption, re-sync all clients with encryption off, then re-enable with a new password. This is intentional by design.
Frequently asked questions
- Can I use SQLite instead of PostgreSQL?
- Joplin Server supports SQLite for local testing only. The project explicitly recommends PostgreSQL for any multi-user or production deployment due to concurrency limitations in SQLite.
- How do I create additional user accounts?
- Log in as admin, go to Admin → Users → Add user. Each user gets their own isolated note store. You can set storage quotas per user from the same page.
- Does E2EE protect attachments as well as note text?
- Yes. Since Joplin 1.6, both note bodies and file attachments are encrypted on the client before upload. Note metadata including titles and notebook names is also encrypted as of the 2.x series.
- What happens if I lose the E2EE master password?
- There is no server-side recovery mechanism by design. You must disable encryption across all clients, re-sync unencrypted data, then re-enable E2EE with a new password. Keep the master password in a password manager.
- How do I back up the Joplin Server data?
- Back up the db_data Docker volume, which contains the PostgreSQL data directory. Use pg_dump inside the container for a logical backup: docker compose exec db pg_dump -U joplin joplin > joplin_backup.sql. Run this on a schedule via a systemd timer or cron.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.