#!/usr/bin/env bash # Install Joplin - free, open-source, self-hostable note-taking app. # Available on Linux (desktop), Android, iOS, Windows, macOS. # Supports Markdown, end-to-end encryption, and self-hosted sync. # # This script: # 1. Installs Joplin desktop app (AUR) # 2. Optionally sets up Joplin Server via Docker for self-hosted sync # # Usage: ./install_joplin.sh [--with-server] # # Android app: https://play.google.com/store/apps/details?id=net.cozic.joplin # or via F-Droid set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } WITH_SERVER=false JOPLIN_SERVER_PORT=22300 JOPLIN_DATA_DIR="$HOME/.joplin-server" DUCKDNS_DOMAIN="" DUCKDNS_TOKEN="" for arg in "$@"; do case "$arg" in --with-server) WITH_SERVER=true ;; --help | -h) echo "Usage: $0 [--with-server]" echo "" echo "Options:" echo " --with-server Also set up Joplin Server via Docker" echo " --help, -h Show this help message" exit 0 ;; *) error "Unknown argument: $arg" exit 1 ;; esac done # ── Check prerequisites ───────────────────────────────────────────── command -v pacman >/dev/null 2>&1 || { error "pacman not found. This script is for Arch Linux." exit 1 } # ── Install Joplin Desktop ────────────────────────────────────────── install_joplin_desktop() { if [[ -f "$HOME/.joplin/Joplin.AppImage" ]]; then info "Joplin desktop is already installed at $HOME/.joplin/Joplin.AppImage" return fi info "Installing Joplin desktop app via official installer (AppImage)..." # Official Joplin install script downloads the latest AppImage wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh | bash info "Joplin desktop installed at ~/.joplin/Joplin.AppImage" info "Launch with: ~/.joplin/Joplin.AppImage (or 'joplin-desktop' from menu)" } # ── 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" <> "$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" </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" </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" } # ── Main ───────────────────────────────────────────────────────────── main() { echo "╔══════════════════════════════════════════════╗" echo "║ Joplin Installation Script ║" echo "║ Free & Open Source Note-Taking App ║" echo "║ https://joplinapp.org ║" echo "╚══════════════════════════════════════════════╝" echo "" install_joplin_desktop if [[ "$WITH_SERVER" == true ]]; then setup_joplin_server else echo "" info "Tip: Run with --with-server to also set up Joplin Server" info "for self-hosted sync across devices (desktop + Android)." fi echo "" info "Android app available at:" info " Google Play: https://play.google.com/store/apps/details?id=net.cozic.joplin" info " F-Droid: https://f-droid.org/packages/net.cozic.joplin/" echo "" info "Done!" } main