From 4f37aad321382d8048a6b1b73ee496d9d9a812da Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 7 Feb 2026 19:39:08 +0100 Subject: [PATCH] feat: install joplin script --- linux_configuration/scripts/install_joplin.sh | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100755 linux_configuration/scripts/install_joplin.sh diff --git a/linux_configuration/scripts/install_joplin.sh b/linux_configuration/scripts/install_joplin.sh new file mode 100755 index 0000000..e3e5001 --- /dev/null +++ b/linux_configuration/scripts/install_joplin.sh @@ -0,0 +1,293 @@ +#!/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