$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

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.

  1. Go to Tools → Options → Encryption (desktop) or Configuration → Encryption (mobile).
  2. Click Enable encryption and set a strong master password. Store this password somewhere safe — it cannot be recovered.
  3. 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 be 127.0.0.1:22300:22300, not 0.0.0.0.
  • Database connection refused: The depends_on healthcheck should prevent this, but if Postgres takes too long on slow storage, increase the healthcheck retries value.
  • 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_size in Nginx and reload. Joplin's default attachment limit is also configurable via the MAX_TIME_DRIFT and 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.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

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