diff --git a/.dockerignore b/.dockerignore index 6174672..aab752e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ data +mangas .git .env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b5e494d --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# Copia a .env y ajusta. Todas son opcionales. + +# Usuario/grupo dueño de ./data y de la carpeta de mangas en el host (salida de `id -u` / `id -g`). +PUID=1000 +PGID=1000 + +# Dónde guardar los mangas descargados en el host (por defecto ./mangas dentro del proyecto). +MANGA_DIR_HOST=/zdata/media/mangas + +# Interfaz y puerto publicados. 0.0.0.0 = todas. +BIND_IP=0.0.0.0 +HOST_PORT=3352 + +# URL de la web que se usa en los enlaces de las notificaciones. +PUBLIC_URL=http://192.168.1.141:3352 + +# Registro de cuentas nuevas (con la base vacía, el primer usuario siempre puede registrarse). +ALLOW_SIGNUP=false + +TZ=America/El_Salvador +CHECK_INTERVAL_HOURS=3 diff --git a/.gitignore b/.gitignore index ec51d9e..ff4375d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ # Datos de ejecución: usuarios, sesiones, historial y clave HMAC del proxy de imágenes data/* !data/.keep +# Descargas cuando se usan dentro del proyecto (MANGA_DIR_HOST por defecto) +mangas/* +!mangas/.keep .env +.env.* +!.env.example node_modules/ diff --git a/Dockerfile b/Dockerfile index 14a7a1d..7f55891 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,11 @@ -FROM node:24-alpine -ENV NODE_ENV=production NODE_NO_WARNINGS=1 TZ=America/El_Salvador +# Versión fijada: node:sqlite aún es experimental y una actualización silenciosa podría romperlo. +FROM node:24.22.2-alpine3.24 +ENV NODE_ENV=production NODE_NO_WARNINGS=1 RUN apk add --no-cache tzdata WORKDIR /app COPY app/ ./ +# Usuario por defecto; docker-compose.yml lo sustituye por PUID:PGID. USER node EXPOSE 3352 -HEALTHCHECK --interval=60s --timeout=5s CMD wget -qO- http://127.0.0.1:3352/api/me >/dev/null || exit 1 +HEALTHCHECK --interval=60s --timeout=5s --start-period=20s CMD wget -qO- http://127.0.0.1:3352/api/me >/dev/null || exit 1 CMD ["node", "server.js"] diff --git a/README.md b/README.md index e449770..0bc57c6 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,36 @@ Lector web de manga autoalojado, con las dos fuentes de ## Uso ```bash +cp .env.example .env # opcional: ajusta rutas, puerto, usuario… docker compose up -d --build -# http://192.168.1.141:3352 (o la IP de Tailscale) +# http://:3352 ``` -Crea tu cuenta en la pantalla de inicio. Para cerrar el registro después: -`ALLOW_SIGNUP=false` en `.env` y `docker compose up -d`. +El contenedor no depende del sistema operativo: imagen Node fijada (`node:24.22.2-alpine3.24`), +sin dependencias npm, y todo lo propio de la máquina está en `.env` (ver `.env.example`). +Sin `.env` arranca igual: puerto 3352 en todas las interfaces y descargas en `./mangas`. -Copia de seguridad: basta con `data/manga.db` (+ `-wal`/`-shm` si existen). +El primer usuario puede registrarse siempre; después el registro queda cerrado salvo +`ALLOW_SIGNUP=true` en `.env`. + +## Qué guardar / mover a otro servidor + +| Qué | Dónde | Contenido | +|---|---|---| +| Código | este repositorio | se recrea con `git clone` | +| `.env` | raíz del proyecto (no está en git) | rutas, puerto, PUID/PGID, URL pública | +| `data/` | raíz del proyecto (no está en git) | `manga.db` (+ `-wal`/`-shm`): usuarios, historial, estadísticas, seguimiento, ajustes; `secret`: clave del proxy de imágenes | +| Mangas descargados | `MANGA_DIR_HOST` (aquí `/zdata/media/mangas`) | CBZ + portadas | + +Pasos en el sistema nuevo: + +```bash +git clone manga-reader && cd manga-reader +# copiar .env y data/ desde el servidor anterior (con el contenedor parado), y la carpeta de mangas +id -u; id -g # si no son 1000, ponlos en PUID/PGID del .env +docker compose up -d --build +``` + +Si el usuario del host no coincide con PUID/PGID, el contenedor no arranca y el log dice +exactamente qué `chown` hacer. Si la carpeta de mangas no existe, Docker la crea como root: +créala antes (`mkdir -p` con tu usuario) para que las descargas funcionen. diff --git a/app/server.js b/app/server.js index 986f838..d0cb3e5 100644 --- a/app/server.js +++ b/app/server.js @@ -20,7 +20,24 @@ const UA = 'manga-reader-selfhosted/1.0' const SESSION_DAYS = 120 // ---------- Base de datos ---------- -fs.mkdirSync(DATA_DIR, { recursive: true }) +// Comprobación de permisos al arrancar: con un UID distinto en el host, el error de SQLite sería críptico. +function checkWritable(dir, fatal) { + try { + fs.mkdirSync(dir, { recursive: true }) + const probe = path.join(dir, `.write-test-${process.pid}`) + fs.writeFileSync(probe, '') + fs.rmSync(probe) + return true + } catch (e) { + const uid = process.getuid?.(), gid = process.getgid?.() + console.error(`${fatal ? 'ERROR' : 'AVISO'}: no se puede escribir en ${dir} (${e.code || e.message}) como uid ${uid}:${gid}. + Solución: en el host, "sudo chown -R ${uid}:${gid} " o pon PUID/PGID en .env con el dueño de la carpeta (id -u / id -g).`) + if (fatal) process.exit(1) + return false + } +} +checkWritable(DATA_DIR, true) +checkWritable(MANGA_DIR, false) // sin esto solo fallan las descargas const db = new DatabaseSync(path.join(DATA_DIR, 'manga.db')) const SECRET_FILE = path.join(DATA_DIR, 'secret') if (!fs.existsSync(SECRET_FILE)) fs.writeFileSync(SECRET_FILE, crypto.randomBytes(32).toString('hex'), { mode: 0o600 }) diff --git a/docker-compose.yml b/docker-compose.yml index caa6d2c..2027be8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,33 @@ +# Todo lo que depende de la máquina va en .env (ver .env.example). +# Sin .env también arranca: puerto 3352 en todas las interfaces, descargas en ./mangas. services: manga-reader: build: . + image: manga-reader:local container_name: manga-reader restart: unless-stopped + # Dueño de ./data y de la carpeta de mangas en el host (id -u / id -g). + user: "${PUID:-1000}:${PGID:-1000}" environment: - - TZ=America/El_Salvador + - TZ=${TZ:-America/El_Salvador} - PORT=3352 - DATA_DIR=/app/data - MANGA_DIR=/mangas # Cada cuántas horas se buscan capítulos nuevos de los mangas seguidos. - CHECK_INTERVAL_HOURS=${CHECK_INTERVAL_HOURS:-3} # URL con la que se abre la web desde las notificaciones. - - PUBLIC_URL=${PUBLIC_URL:-http://192.168.1.141:3352} - # Pon "false" cuando ya hayas creado las cuentas que necesites. - # (Si no hay ningún usuario, el primero siempre puede registrarse.) - - ALLOW_SIGNUP=${ALLOW_SIGNUP:-true} + - PUBLIC_URL=${PUBLIC_URL:-http://localhost:3352} + # Registro de cuentas nuevas. Con la base vacía el primer usuario siempre puede registrarse. + - ALLOW_SIGNUP=${ALLOW_SIGNUP:-false} volumes: - # SQLite con usuarios, sesiones e historial de lectura. + # SQLite con usuarios, sesiones, historial, estadísticas y clave del proxy de imágenes. - ./data:/app/data # Mangas descargados: /Capitulo NNNN.cbz + cover.jpg - - /zdata/media/mangas:/mangas + - ${MANGA_DIR_HOST:-./mangas}:/mangas ports: - # Local del servidor, LAN y Tailscale. - - "127.0.0.1:3352:3352" - - "192.168.1.141:3352:3352" - - "${IP_TAILSCALE:-100.125.134.51}:3352:3352" + # 0.0.0.0 no depende de que la IP de la LAN o de Tailscale existan al arrancar. + # Para limitarlo a una interfaz: BIND_IP=192.168.1.141 en .env. + - "${BIND_IP:-0.0.0.0}:${HOST_PORT:-3352}:3352" read_only: true tmpfs: - /tmp diff --git a/mangas/.keep b/mangas/.keep new file mode 100644 index 0000000..e69de29