testsAndMisc/linux_configuration/scripts/single_use/lib/joplin_server.sh
Krzysztof kuhy Rudnicki dadd33f82d
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Split install_joplin.sh and establish the shell-split pattern
299 -> 103, with the --with-server half in lib/joplin_server.sh. First
of the 84 linux_configuration files, done slowly to settle the pattern.

Three things bash -n and shellcheck do not catch, all found here:

- SCRIPT_DIR from "$0" breaks when the entry script is itself sourced.
  Use "${BASH_SOURCE[0]}"; keep readlink -f for the one symlinked script.
- A lib needs a shebang AND +x: without the shebang shellcheck raises
  SC2148, with it the shebang-executable hook fails. Match common.sh.
- Only a stubbed run proves the source line resolves. Copy entry+lib to
  a mktemp -d, sed `main` into a declare -F probe, run that. No
  installer step executes.

Also enumerates which scripts escape the repo: three copied out by
install_pacman_hooks.sh, one symlink, everything else generated. All
recorded in docs/shell-split-recipes.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:33:59 +02:00

210 lines
6.7 KiB
Bash
Executable File

#!/bin/bash
# Joplin Server setup: DuckDNS dynamic DNS plus the Docker compose stack.
#
# Sourced by install_joplin.sh when it is run with --with-server; split out to
# keep that script under the 250-line cap. Expects the caller to have defined
# info/warn/error and the JOPLIN_* and DUCKDNS_* variables. Sourced, not run,
# so it inherits the caller's strict mode; the shebang is for shellcheck and
# matches scripts/lib/common.sh.
# ── Set up DuckDNS for stable URL ───────────────────────────────────
setup_duckdns() {
info "Setting up DuckDNS for stable server URL..."
if [[ -z "$DUCKDNS_DOMAIN" ]]; then
echo ""
info "Your public IP may change. DuckDNS provides a free stable hostname."
info "1. Go to https://www.duckdns.org/ and sign in (Google/GitHub/etc.)"
info "2. Create a subdomain (e.g. 'myjoplin' for myjoplin.duckdns.org)"
info "3. Copy your token from the DuckDNS dashboard"
echo ""
read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN
read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN
fi
if [[ -z "$DUCKDNS_DOMAIN" ]] || [[ -z "$DUCKDNS_TOKEN" ]]; then
warn "DuckDNS not configured. Falling back to raw public IP (may change!)."
return 1
fi
local full_domain="${DUCKDNS_DOMAIN}.duckdns.org"
# Update DuckDNS now
info "Updating DuckDNS record for ${full_domain}..."
local result
result=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=")
if [[ "$result" == "OK" ]]; then
info "DuckDNS updated successfully: ${full_domain}"
else
warn "DuckDNS update returned: $result"
fi
# Set up cron job to keep IP updated every 5 minutes
local duckdns_script="$JOPLIN_DATA_DIR/duckdns-update.sh"
cat >"$duckdns_script" <<DUCKEOF
#!/usr/bin/env bash
result=\$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=")
echo "\$(date): \$result" >> "$JOPLIN_DATA_DIR/duckdns.log"
DUCKEOF
chmod +x "$duckdns_script"
# Add cron job (remove old one if exists, add new)
(
crontab -l 2>/dev/null | grep -v "duckdns-update.sh"
echo "*/5 * * * * $duckdns_script"
) | crontab -
info "DuckDNS cron job installed (updates every 5 minutes)"
# Save config for future runs
local config_file="$JOPLIN_DATA_DIR/.duckdns.conf"
cat >"$config_file" <<CONFEOF
DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN}"
DUCKDNS_TOKEN="${DUCKDNS_TOKEN}"
CONFEOF
chmod 600 "$config_file"
return 0
}
# ── Set up Joplin Server (Docker) ───────────────────────────────────
setup_joplin_server() {
info "Setting up Joplin Server via Docker..."
# Ensure Docker is installed and running
if ! command -v docker >/dev/null 2>&1; then
info "Installing Docker..."
sudo pacman -S --needed --noconfirm docker docker-compose
fi
if ! systemctl is-active --quiet docker; then
info "Starting Docker service..."
sudo systemctl enable --now docker
fi
# Add user to docker group if not already a member
if ! groups | grep -q '\bdocker\b'; then
warn "Adding $USER to docker group (re-login required for group to take effect)."
sudo usermod -aG docker "$USER"
fi
# Create data directory
mkdir -p "$JOPLIN_DATA_DIR"
local compose_file="$JOPLIN_DATA_DIR/docker-compose.yml"
# Load saved DuckDNS config if it exists
if [[ -f "$JOPLIN_DATA_DIR/.duckdns.conf" ]]; then
# shellcheck source=/dev/null
source "$JOPLIN_DATA_DIR/.duckdns.conf"
fi
# Set up DuckDNS for a stable hostname
local server_url
if setup_duckdns; then
server_url="http://${DUCKDNS_DOMAIN}.duckdns.org:${JOPLIN_SERVER_PORT}"
info "Using stable DuckDNS URL: $server_url"
else
# Fallback to public IP
local host_ip
host_ip="$(curl -s --max-time 5 ifconfig.me 2>/dev/null)"
if [[ -z "$host_ip" ]]; then
host_ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')"
fi
if [[ -z "$host_ip" ]]; then
host_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
fi
if [[ -z "$host_ip" ]]; then
warn "Could not detect external IP. Falling back to 0.0.0.0"
host_ip="0.0.0.0"
fi
server_url="http://${host_ip}:${JOPLIN_SERVER_PORT}"
warn "Using raw IP URL (may change!): $server_url"
fi
cat >"$compose_file" <<EOF
version: "3"
services:
joplin-db:
image: postgres:16
container_name: joplin-db
restart: unless-stopped
volumes:
- joplin-db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: joplin
POSTGRES_USER: joplin
POSTGRES_PASSWORD: changeme
networks:
- joplin-net
joplin-server:
image: joplin/server:latest
container_name: joplin-server
restart: unless-stopped
depends_on:
- joplin-db
ports:
- "0.0.0.0:${JOPLIN_SERVER_PORT}:22300"
environment:
APP_PORT: 22300
APP_BASE_URL: "${server_url}"
DB_CLIENT: pg
POSTGRES_HOST: joplin-db
POSTGRES_PORT: 5432
POSTGRES_DATABASE: joplin
POSTGRES_USER: joplin
POSTGRES_PASSWORD: changeme
networks:
- joplin-net
volumes:
joplin-db-data:
networks:
joplin-net:
EOF
info "Starting Joplin Server..."
docker compose -f "$compose_file" up -d
echo ""
info "Joplin Server is running at: ${server_url}"
echo ""
echo " Default admin credentials:"
echo " Email: admin@localhost"
echo " Password: admin"
echo ""
warn "IMPORTANT: Change the default admin password and the database"
warn "password (POSTGRES_PASSWORD in $compose_file) immediately!"
echo ""
info "Firewall: opening port ${JOPLIN_SERVER_PORT}/tcp for external access..."
if command -v ufw >/dev/null 2>&1; then
sudo ufw allow "${JOPLIN_SERVER_PORT}/tcp" || warn "Could not configure ufw"
elif command -v firewall-cmd >/dev/null 2>&1; then
if sudo firewall-cmd --permanent --add-port="${JOPLIN_SERVER_PORT}/tcp" &&
sudo firewall-cmd --reload; then
:
else
warn "Could not configure firewalld"
fi
elif command -v iptables >/dev/null 2>&1; then
sudo iptables -A INPUT -p tcp --dport "${JOPLIN_SERVER_PORT}" -j ACCEPT || warn "Could not configure iptables"
else
warn "No firewall tool found. Ensure port ${JOPLIN_SERVER_PORT}/tcp is open manually."
fi
echo ""
echo " To connect Joplin desktop/Android to this server:"
echo " 1. Open Joplin → Tools → Options → Synchronisation"
echo " 2. Set target to 'Joplin Server'"
echo " 3. Enter URL: ${server_url}"
echo " 4. Enter your Joplin Server email and password"
echo ""
echo " Server management:"
echo " Start: docker compose -f $compose_file up -d"
echo " Stop: docker compose -f $compose_file down"
echo " Logs: docker compose -f $compose_file logs -f"
echo " Update: docker compose -f $compose_file pull && docker compose -f $compose_file up -d"
}