An installer and runtime layout for Joplin Server behind NGINX with optional Let’s Encrypt, optional email (SMTP), and daily backups.
- What you get
- Requirements
- Quick Start
- Flags / Configuration
- Directory Layout
- Email (optional)
- TLS / Let’s Encrypt
- Backups
- Operations
- Security / Hardening
- Firewall / SELinux / AppArmor
- Troubleshooting
- Uninstall / Full Cleanup
- License
- Postgres 14 with a proper healthcheck.
- Joplin Server (latest image) with:
- Configurable
APP_BASE_URL - Optional SMTP mailer (
relayormailbox) or disabled mail. - A Node-based healthcheck (works even in minimalist images).
- Configurable
- NGINX reverse proxy, HTTP→HTTPS redirect, HTTP/2 enabled.
- Let’s Encrypt (webroot) certificates (optional).
- Backups: daily cron (optional), with
pg_dump(custom + plain SQL) and config snapshots. - A Compose wrapper (
compose.sh) so commands work whether your host hasdocker composeor legacydocker-compose.
- A Linux x86_64/arm64 host with outbound Internet.
- DNS A/AAAA records for your
--domainpointing to this host. - Open firewall ports:
- 80/tcp for HTTP (and Let’s Encrypt HTTP-01 challenge)
- 443/tcp for HTTPS
sudoprivileges (for installing Docker)
The script will install Docker CE for most common distros (Debian/Ubuntu, RHEL/Rocky/Alma/Fedora, openSUSE/SLES, Arch).
Browse to the directory with joplin-deploy.sh and run one of the following.
sudo bash joplin-deploy.sh \
--domain joplin.example.com \
--le-email you@example.com \
--db-pass 'SuperSecret' \
--enable-backup- Installs Docker CE + Compose plugin (if missing)
- Sets up the stack under
/opt/joplin-stack - Issues a Let’s Encrypt cert via webroot
- Installs daily backup + renew cron
- No email configuration
Minimal example (no LE, use your own certs):
sudo bash joplin-deploy.sh \
--domain joplin.example.com \
--no-letsencrypt \
--db-pass 'SuperSecret' \
--enable-backup- Then place certs:
- /opt/joplin-stack/letsencrypt/live/joplin.example.com/fullchain.pem
- /opt/joplin-stack/letsencrypt/live/joplin.example.com/privkey.pem
- Reload nginx:
- cd /opt/joplin-stack && ./compose.sh --env-file ./.env restart nginx
Mail relay configuration example:
sudo bash joplin-deploy.sh \
--domain joplin.example.com \
--le-email you@example.com \
--smtp-mode relay \
--smtp-host smtp-relay.gmail.com --smtp-port 587 --smtp-security starttls \
--noreply-email noreply@example.com \
--db-pass 'SuperSecret' \
--enable-backupMail mailbox configuration example:
sudo bash joplin-deploy.sh \
--domain joplin.example.com \
--le-email you@example.com \
--smtp-mode mailbox \
--smtp-host smtp.gmail.com --smtp-port 587 --smtp-security starttls \
--smtp-user noreply@example.com --smtp-pass 'APP_PASSWORD' \
--noreply-email noreply@example.com \
--db-pass 'SuperSecret'Key flags (full list in --help):
| Flag | Required | Default | Notes |
|---|---|---|---|
--domain |
✅ | — | Public FQDN (A/AAAA must point to the host) |
--db-pass |
✅* | autogenerated | Postgres password. If omitted, a strong random password is generated and placed in .env |
--le-email |
☑️ | — | Required if using Let’s Encrypt (default ON) |
--no-letsencrypt |
off | Disable LE (bring your own certs) | |
--smtp-mode |
none |
none | relay | mailbox |
|
--smtp-host |
— | SMTP hostname (required for relay/mailbox) |
|
--smtp-user / --smtp-pass |
— | Required for mailbox (and relay if your relay is authenticated) |
|
--smtp-port |
587 |
Typically 587 with starttls or 465 with tls |
|
--smtp-security |
starttls |
starttls | tls | none |
|
--noreply-email |
noreply@localhost |
From address used by Joplin | |
--enable-backup |
off | Installs daily backup (02:30) and LE renew (03:05) | |
--no-cron |
off | Skip all cron installation | |
--stack-dir |
/opt/joplin-stack |
Override deployment directory |
Environment is written to ${STACK_DIR}/.env. The Compose file uses these variables for portability.
Rootful (default): /opt/joplin-stack
joplin-stack/
├── .env
├── compose.sh # wrapper (docker compose / docker-compose)
├── docker-compose.yml
├── nginx/
│ └── conf.d/joplin.conf
├── letsencrypt/
│ └── live/<domain>/{fullchain.pem,privkey.pem}
├── data/ # Postgres data directory
└── backups/
├── YYYY-MM-DD_HH-MM-SS/
│ ├── postgres_joplin_*.dump
│ ├── postgres_joplin_*.sql.gz
│ ├── docker-compose.yml
│ ├── .env
│ └── nginx/...
└── cron.log
Email is disabled by default. To enable, choose one:
--smtp-mode relay \
--smtp-host smtp-relay.gmail.com --smtp-port 587 --smtp-security starttls \
--noreply-email noreply@example.comYour relay may be IP-allowlisted (no auth) or require auth (then add
--smtp-user/--smtp-pass).
--smtp-mode mailbox \
--smtp-host smtp.gmail.com --smtp-port 587 --smtp-security starttls \
--smtp-user noreply@example.com --smtp-pass 'APP_PASSWORD' \
--noreply-email noreply@example.comIn both cases, the script sets MAILER_ENABLED=1. If --smtp-mode none, MAILER_ENABLED=0 and all SMTP fields are harmlessly present but unused.
- Webroot HTTP-01 challenge on port 80.
- Certificates are stored under
${STACK_DIR}/letsencrypt/and mounted into Nginx at/etc/letsencrypt. - Daily renew cron at 03:05 will run
certbot renewand reload Nginx.
Bring-your-own certs: place files at:
${STACK_DIR}/letsencrypt/live/<domain>/fullchain.pem
${STACK_DIR}/letsencrypt/live/<domain>/privkey.pem
Then reload Nginx:
cd ${STACK_DIR} && ./compose.sh --env-file ./.env restart nginxIf --enable-backup is provided, the installer drops backup-joplin.sh and a cron entry (02:30). The script:
- Runs
pg_dump(custom format.dump) and a plain SQL (.sql.gz). - Copies
docker-compose.yml,.env, and the entirenginx/directory. - Writes checksums (
SHA256SUMS) and prunes backups older than 14 days.
Run ad-hoc:
cd ${STACK_DIR}
bash backup-joplin.shFrom a custom dump (preferred):
# Recreate DB (if needed)
./compose.sh exec -T joplin_postgres sh -lc 'psql -U "$POSTGRES_USER" -c "DROP DATABASE IF EXISTS joplin; CREATE DATABASE joplin;"'
# Copy and restore
docker cp backups/<timestamp>/postgres_joplin_*.dump joplin_postgres:/tmp/restore.dump
./compose.sh exec -T joplin_postgres sh -lc 'export PGPASSWORD="$POSTGRES_PASSWORD"; pg_restore -U "$POSTGRES_USER" -d joplin --clean --if-exists /tmp/restore.dump && rm -f /tmp/restore.dump'From the plain SQL:
zcat backups/<timestamp>/postgres_joplin_*.sql.gz \
| ./compose.sh exec -T joplin_postgres sh -lc 'export PGPASSWORD="$POSTGRES_PASSWORD"; psql -U "$POSTGRES_USER" -d joplin'cd ${STACK_DIR}
./compose.sh --env-file ./.env ps
./compose.sh --env-file ./.env logs -f app
./compose.sh --env-file ./.env logs -f nginxcd ${STACK_DIR}
./compose.sh --env-file ./.env pull app
./compose.sh --env-file ./.env up -d app- Edit
${STACK_DIR}/.env(SMTP vars,DOMAIN,APP_BASE_URLis derived fromDOMAINvia compose). - If domain changes, issue new certs:
cd ${STACK_DIR} ./compose.sh --env-file ./.env run --rm certbot certonly --webroot -w /var/www/certbot -d NEWDOMAIN --email you@example.com --agree-tos --no-eff-email ./compose.sh --env-file ./.env exec -T nginx nginx -s reload
- Redeploy app and nginx:
./compose.sh --env-file ./.env up -d app nginx
- Use strong, unique
--db-pass. The script autogenerates one if omitted. - Set appropriate
MAILER_NOREPLY_EMAILand use provider recommendations (app passwords for Gmail). - Consider restricting Nginx to TLS 1.3 only if all clients support it.
- Keep backups encrypted at rest if storing off-host (add
gpg/agein the backup script if needed).
- Firewall: open 80/tcp and 443/tcp on the host. Example (UFW):
sudo ufw allow 80/tcp sudo ufw allow 443/tcp
- SELinux: if enforcing and you use rootful with host paths outside standard locations, you may need appropriate contexts for mounted dirs.
- AppArmor: default profiles are fine; you can set custom profiles for Nginx/Postgres if required by your policy.
The app healthcheck uses Node’s net module to test 127.0.0.1:22300. If unhealthy:
- Check app logs:
cd ${STACK_DIR} ./compose.sh --env-file ./.env logs -f app
- Verify Postgres connection details (
DB_*in.env). - Ensure the database container is healthy and reachable as
db:5432inside theappnetwork.
- App probably isn’t listening yet or failing to start.
- Confirm from within the Nginx container:
cd ${STACK_DIR} ./compose.sh exec -T nginx sh -lc 'wget -qO- http://app:22300/ || echo FAIL'
- Check Joplin’s admin emails page (
/admin/emails) and the app logs. - Verify
MAILER_ENABLEDis1and SMTP settings are correct for your provider.
Warning: This removes containers, volumes, certs, and database data (if you also delete
data/). Make backups first.
cd ${STACK_DIR}
./compose.sh --env-file ./.env down
# Optionally remove images/networks/volumes related to the stack
docker network prune
docker volume ls | awk '/joplin/ {print $2}' | xargs -r docker volume rm
# Remove directory (certs, DB, backups)
rm -rf ${STACK_DIR}This installer and README are provided under the MIT License. Joplin Server and all container images are licensed by their respective authors.