El cuaderno de notas de elSultan

Las aplicaciones que tengo en el NAS (2026)

Publicado el: por elSultan

Actualizado el: • 7 min de lectura

El NAS lo uso sobre todo para temas multimedia (películas, series y música), además de como backup y almacén de fotos. Como me gusta toquetear e instalo, pruebo y desinstalo aplicaciones constantemente, uso Docker para todo lo que puedo: es sencillo y limpio cuando desinstalas.

Las aplicaciones van y vienen, así que intentaré mantener esto actualizado. Última actualización: octubre de 2026.

Multimedia

  • Descargas: las ArrApps y qBittorrent. Mención especial para aMule, que tras volver con la versión 3.0.0 ha vuelto al NAS y está siempre encendido, aunque descargo poco por ahí.
  • Servir: Jellyfin para vídeo y Navidrome para audio.

Infraestructura

  • Proxy: NPM. Tras probar Traefik, es lo más sencillo que he encontrado. Lo he movido a la RPi: el NAS se reinicia y apaga más que la RPi, y eso fue uno de los motivos principales.
  • Docker: Dockge para manejar los stacks y Dozzle para ver los logs.
  • Dashboard: Homepage. Fácil de configurar y suficiente; la tengo siempre abierta en el navegador.

Backup y sincronización

  • Syncthing. Un disco duro en mi ordenador sincronizado con el NAS para tener copia, y algunas cosas en el móvil.

Fotos

  • Immich. Ya es fijo y lo uso tanto desde Android como desde iOS. Contento con él.

Notas y RSS

  • RSS: FreshRSS. Indispensable.
  • Notas: ahora mismo no uso wikis. He pasado por varias y ninguna ha terminado de encajar.

Fediverso

Ya no autoalojo GoToSocial. Funcionaba muy bien y estaba muy contento con él, pero cuando tengo que apagar el NAS no puedo acceder a mi cuenta, y eso me fastidiaba. Me mudé a un servidor externo: ahora podéis encontrarme en corteximplant.com/@elsultan.

Monitorización

  • Scrutiny. Monitorización de discos. No le suelo hacer caso, lo miro muy de vez en cuando, pero ahí está para cuando toca.

Por si acaso

Aplicaciones que no uso a diario pero que tengo en el stack porque algún día me han salvado o me pueden salvar:

  • it-tools. Utilidades para el navegador. No lo uso mucho, pero me ha evitado andar buscando herramientas online.
  • Flaresolverr. No tengo muy claro que funcione, pero ahí está.
  • Hister. Con su extensión guarda todas las páginas que visito y me permite buscar en ellas. No lo uso mucho, pero me ha salvado alguna vez de buscar algo que ya había visitado.

Probados y descartados

Aplicaciones que he probado y ya no uso. Las dejo aquí por si a alguien le sirve la experiencia:

  • beszel. Monitorización del NAS y la RPi. No me convenció.
  • changedetection. Un intento de monitorización que no funcionó y ya no necesito.
  • gotify. Notificaciones push. Tampoco funcionó como esperaba.
  • recyclarr. Sincronización de perfiles de calidad para las arr. Usado una vez y no más: demasiado lío para algo que configuro una única vez y luego va incluido en el backup.
  • jellyseerr. Gestión de peticiones para Jellyfin. Lo he usado, pero ya no: tardan menos los niños diciéndome en vivo lo que quieren que entrar ahí.
  • Jackett. No lo uso. Para indexadores, Prowlarr es suficiente.
  • Linkding. Marcadores. Borrado, ya no lo uso.
  • Filebrowser. Navegador de archivos. Borrado, ya no lo uso.

Cómo actualizo los stacks

Tengo un script propio, updateDocker.sh, que lanzo desde la carpeta que contiene todos mis stacks. Hace lo siguiente:

  1. Detecta si tiene permisos de Docker (si no, se eleva con sudo).
  2. Detecta si uso docker compose o docker-compose.
  3. Recorre los subdirectorios del directorio actual buscando un archivo compose.
  4. Para cada stack, comprueba si tiene contenedores en ejecución. Si no está corriendo, lo salta.
  5. Hace pull de las imágenes nuevas y luego up -d --remove-orphans.
  6. Al final, limpia imágenes sin usar con docker image prune -f.
  7. Muestra un resumen con actualizados, saltados y fallidos.

Uso:

# Actualiza todos los stacks del directorio actual
./updateDocker.sh

# Actualiza todos los stacks de un directorio concreto
./updateDocker.sh ~/misStacks

# Actualiza solo algunos stacks
./updateDocker.sh ~/misStacks jellyfin navidrome

# Simulación, sin cambios
./updateDocker.sh --dry-run

# Sin limpiar imágenes al terminar
./updateDocker.sh --no-prune

El script completo, por si a alguien le sirve:

#!/bin/bash
# =============================================================================
#  update-docker.sh — Actualizador de stacks Docker Compose
#  Uso: ./update-docker.sh [DIRECTORIO_BASE] [--dry-run] [--no-prune] [stack...]
# =============================================================================

set -euo pipefail

# ── Colores ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

info()    { echo -e "${CYAN}${BOLD}[INFO]${RESET}  $*"; }
ok()      { echo -e "${GREEN}${BOLD}[ OK ]${RESET}  $*"; }
warn()    { echo -e "${YELLOW}${BOLD}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}${BOLD}[ERR ]${RESET}  $*" >&2; }
section() {
    echo -e "\n${BLUE}${BOLD}┌──────────────────────────────────────┐${RESET}"
    echo -e "${BLUE}${BOLD}│  $*${RESET}"
    echo -e "${BLUE}${BOLD}└──────────────────────────────────────┘${RESET}"
}

# ── Defaults ──────────────────────────────────────────────────────────────────
BASE_DIR="$(pwd)"
DRY_RUN=false
DO_PRUNE=true
SELECTED_STACKS=()

# Contadores
UPDATED=0
SKIPPED=0
FAILED=0
FAILED_STACKS=()

# ── Elevación de privilegios ──────────────────────────────────────────────────
elevate() {
    [[ $EUID -eq 0 ]] && return
    # Comprobar si el usuario puede usar docker sin sudo
    if docker info &>/dev/null 2>&1; then
        return
    fi
    info "Se requieren privilegios para acceder a Docker."
    if ! sudo -v; then
        error "Autenticación fallida. Abortando."
        exit 1
    fi
    exec sudo "$0" "$@"
}

# ── Detectar binario docker compose ──────────────────────────────────────────
detect_compose() {
    if docker compose version &>/dev/null 2>&1; then
        echo "docker compose"
    elif command -v docker-compose &>/dev/null; then
        echo "docker-compose"
    else
        error "No se encontró 'docker compose' ni 'docker-compose'."
        exit 1
    fi
}

# ── Ayuda ─────────────────────────────────────────────────────────────────────
usage() {
    echo -e "${BOLD}Uso:${RESET} $0 [DIRECTORIO_BASE] [opciones] [stack1 stack2 ...]"
    echo
    echo -e "${BOLD}Opciones:${RESET}"
    echo "  --dry-run      Muestra qué se haría sin ejecutar nada"
    echo "  --no-prune     No eliminar imágenes sin usar tras actualizar"
    echo "  --help, -h     Muestra esta ayuda"
    echo
    echo -e "${BOLD}Ejemplos:${RESET}"
    echo "  $0                          # Actualiza todos los stacks en el directorio actual"
    echo "  $0 ~/docker                 # Actualiza todos los stacks en ~/docker"
    echo "  $0 ~/docker nextcloud plex  # Actualiza solo nextcloud y plex"
    echo "  $0 --dry-run                # Simulación sin cambios"
    exit 0
}

# ── Comprobar si un stack tiene contenedores en ejecución ────────────────────
is_stack_running() {
    local compose_file="$1"
    local running
    running=$($COMPOSE -f "$compose_file" ps --status running --quiet 2>/dev/null)
    [[ -n "$running" ]]
}

# ── Actualizar un stack ───────────────────────────────────────────────────────
update_stack() {
    local stack_dir="$1"
    local stack_name
    stack_name=$(basename "$stack_dir")
    local compose_file=""

    # Buscar archivo compose (varios nombres posibles)
    for name in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
        if [[ -f "$stack_dir/$name" ]]; then
            compose_file="$stack_dir/$name"
            break
        fi
    done

    if [[ -z "$compose_file" ]]; then
        warn "[$stack_name] No se encontró archivo compose. Saltando."
        ((SKIPPED++)) || true
        return
    fi

    # Saltar stacks que no tienen contenedores en ejecución
    if ! is_stack_running "$compose_file"; then
        info "[$stack_name] Sin contenedores activos. Saltando."
        ((SKIPPED++)) || true
        return
    fi

    section "Stack: $stack_name"
    info "Archivo: $compose_file"

    if $DRY_RUN; then
        info "[DRY-RUN] Se ejecutaría: $COMPOSE -f $compose_file pull && up -d"
        ((SKIPPED++)) || true
        return
    fi

    # Pull de nuevas imágenes
    info "Descargando imágenes nuevas..."
    if ! $COMPOSE -f "$compose_file" pull; then
        error "[$stack_name] Fallo al hacer pull. Saltando recreación."
        ((FAILED++)) || true
        FAILED_STACKS+=("$stack_name")
        return
    fi

    # Recrear contenedores con nuevas imágenes
    info "Recreando contenedores..."
    if ! $COMPOSE -f "$compose_file" up -d --remove-orphans; then
        error "[$stack_name] Fallo al recrear contenedores."
        ((FAILED++)) || true
        FAILED_STACKS+=("$stack_name")
        return
    fi

    ok "[$stack_name] Actualizado correctamente."
    ((UPDATED++)) || true
}

# ── Limpiar imágenes sin usar ─────────────────────────────────────────────────
prune_images() {
    $DO_PRUNE || return
    $DRY_RUN  && { info "[DRY-RUN] Se ejecutaría: docker image prune -f"; return; }

    section "Limpiando imágenes sin usar"
    local freed
    freed=$(docker image prune -f 2>&1 | grep "reclaimed" || echo "nada")
    ok "Limpieza completada. ($freed)"
}

# ── Resumen ───────────────────────────────────────────────────────────────────
summary() {
    echo
    echo -e "${BLUE}${BOLD}══════════════════════════════════════${RESET}"
    echo -e "${BOLD}  Resumen de actualización${RESET}"
    echo -e "${BLUE}${BOLD}══════════════════════════════════════${RESET}"
    echo -e "  ${GREEN}${BOLD}Actualizados:${RESET}  $UPDATED"
    echo -e "  ${YELLOW}${BOLD}Saltados:${RESET}      $SKIPPED"
    echo -e "  ${RED}${BOLD}Fallidos:${RESET}      $FAILED"

    if [[ ${#FAILED_STACKS[@]} -gt 0 ]]; then
        echo
        warn "Stacks con errores:"
        for s in "${FAILED_STACKS[@]}"; do
            echo -e "    ${RED}✗${RESET} $s"
        done
    fi

    echo -e "${BLUE}${BOLD}══════════════════════════════════════${RESET}"
    echo
}

# ── Main ──────────────────────────────────────────────────────────────────────
main() {
    echo -e "\n${BOLD}${CYAN}╔══════════════════════════════════════╗${RESET}"
    echo -e "${BOLD}${CYAN}║   Docker Compose Updater  v1.0       ║${RESET}"
    echo -e "${BOLD}${CYAN}╚══════════════════════════════════════╝${RESET}\n"

    # Parsear argumentos
    local args=()
    for arg in "$@"; do
        case "$arg" in
            --dry-run)   DRY_RUN=true ;;
            --no-prune)  DO_PRUNE=false ;;
            --help|-h)   usage ;;
            -*)          warn "Opción desconocida: $arg" ;;
            *)           args+=("$arg") ;;
        esac
    done

    # Primer argumento no-flag puede ser el directorio base
    if [[ ${#args[@]} -gt 0 && -d "${args[0]}" ]]; then
        BASE_DIR="${args[0]}"
        args=("${args[@]:1}")
    fi

    # El resto son nombres de stacks específicos
    SELECTED_STACKS=("${args[@]}")

    elevate "$@"

    COMPOSE=$(detect_compose)
    info "Usando: $COMPOSE"
    info "Directorio base: $BASE_DIR"
    $DRY_RUN && warn "Modo DRY-RUN activado — no se realizarán cambios."

    # Recoger subdirectorios con compose
    local stacks=()
    while IFS= read -r -d '' dir; do
        stacks+=("$dir")
    done < <(find -L "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)

    if [[ ${#stacks[@]} -eq 0 ]]; then
        warn "No se encontraron subdirectorios en '$BASE_DIR'."
        exit 0
    fi

    # Filtrar por stacks seleccionados si se indicaron
    for stack_dir in "${stacks[@]}"; do
        local name
        name=$(basename "$stack_dir")

        if [[ ${#SELECTED_STACKS[@]} -gt 0 ]]; then
            local match=false
            for sel in "${SELECTED_STACKS[@]}"; do
                [[ "$sel" == "$name" ]] && match=true && break
            done
            $match || continue
        fi

        update_stack "$stack_dir"
    done

    prune_images
    summary

    [[ $FAILED -gt 0 ]] && exit 1 || exit 0
}

main "$@"