Docker autónomo: configuración del host en .env e imagen fijada
- Puertos, carpeta de mangas, PUID/PGID, URL pública y registro se configuran en .env (plantilla en .env.example); sin .env arranca con valores portables (0.0.0.0:3352, ./mangas). - Publicar en 0.0.0.0 evita fallar al arrancar si la IP de la LAN o de Tailscale aún no existe; el acceso lo siguen limitando las reglas DOCKER-USER del host. - Imagen base fijada a node:24.22.2-alpine3.24 (node:sqlite es experimental). - ALLOW_SIGNUP=false por defecto (el primer usuario puede registrarse igual). - Comprobación de permisos al arrancar con mensaje claro (chown / PUID). - README: qué guardar y cómo mover el servicio a otro servidor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
data
|
data
|
||||||
|
mangas
|
||||||
.git
|
.git
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
# Datos de ejecución: usuarios, sesiones, historial y clave HMAC del proxy de imágenes
|
# Datos de ejecución: usuarios, sesiones, historial y clave HMAC del proxy de imágenes
|
||||||
data/*
|
data/*
|
||||||
!data/.keep
|
!data/.keep
|
||||||
|
# Descargas cuando se usan dentro del proyecto (MANGA_DIR_HOST por defecto)
|
||||||
|
mangas/*
|
||||||
|
!mangas/.keep
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
+5
-3
@@ -1,9 +1,11 @@
|
|||||||
FROM node:24-alpine
|
# Versión fijada: node:sqlite aún es experimental y una actualización silenciosa podría romperlo.
|
||||||
ENV NODE_ENV=production NODE_NO_WARNINGS=1 TZ=America/El_Salvador
|
FROM node:24.22.2-alpine3.24
|
||||||
|
ENV NODE_ENV=production NODE_NO_WARNINGS=1
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY app/ ./
|
COPY app/ ./
|
||||||
|
# Usuario por defecto; docker-compose.yml lo sustituye por PUID:PGID.
|
||||||
USER node
|
USER node
|
||||||
EXPOSE 3352
|
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"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -29,11 +29,36 @@ Lector web de manga autoalojado, con las dos fuentes de
|
|||||||
## Uso
|
## Uso
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cp .env.example .env # opcional: ajusta rutas, puerto, usuario…
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
# http://192.168.1.141:3352 (o la IP de Tailscale)
|
# http://<ip-del-servidor>:3352
|
||||||
```
|
```
|
||||||
|
|
||||||
Crea tu cuenta en la pantalla de inicio. Para cerrar el registro después:
|
El contenedor no depende del sistema operativo: imagen Node fijada (`node:24.22.2-alpine3.24`),
|
||||||
`ALLOW_SIGNUP=false` en `.env` y `docker compose up -d`.
|
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 <repo> 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.
|
||||||
|
|||||||
+18
-1
@@ -20,7 +20,24 @@ const UA = 'manga-reader-selfhosted/1.0'
|
|||||||
const SESSION_DAYS = 120
|
const SESSION_DAYS = 120
|
||||||
|
|
||||||
// ---------- Base de datos ----------
|
// ---------- 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} <carpeta>" 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 db = new DatabaseSync(path.join(DATA_DIR, 'manga.db'))
|
||||||
const SECRET_FILE = path.join(DATA_DIR, 'secret')
|
const SECRET_FILE = path.join(DATA_DIR, 'secret')
|
||||||
if (!fs.existsSync(SECRET_FILE)) fs.writeFileSync(SECRET_FILE, crypto.randomBytes(32).toString('hex'), { mode: 0o600 })
|
if (!fs.existsSync(SECRET_FILE)) fs.writeFileSync(SECRET_FILE, crypto.randomBytes(32).toString('hex'), { mode: 0o600 })
|
||||||
|
|||||||
+14
-11
@@ -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:
|
services:
|
||||||
manga-reader:
|
manga-reader:
|
||||||
build: .
|
build: .
|
||||||
|
image: manga-reader:local
|
||||||
container_name: manga-reader
|
container_name: manga-reader
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
- TZ=America/El_Salvador
|
- TZ=${TZ:-America/El_Salvador}
|
||||||
- PORT=3352
|
- PORT=3352
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
- MANGA_DIR=/mangas
|
- MANGA_DIR=/mangas
|
||||||
# Cada cuántas horas se buscan capítulos nuevos de los mangas seguidos.
|
# Cada cuántas horas se buscan capítulos nuevos de los mangas seguidos.
|
||||||
- CHECK_INTERVAL_HOURS=${CHECK_INTERVAL_HOURS:-3}
|
- CHECK_INTERVAL_HOURS=${CHECK_INTERVAL_HOURS:-3}
|
||||||
# URL con la que se abre la web desde las notificaciones.
|
# URL con la que se abre la web desde las notificaciones.
|
||||||
- PUBLIC_URL=${PUBLIC_URL:-http://192.168.1.141:3352}
|
- PUBLIC_URL=${PUBLIC_URL:-http://localhost:3352}
|
||||||
# Pon "false" cuando ya hayas creado las cuentas que necesites.
|
# Registro de cuentas nuevas. Con la base vacía el primer usuario siempre puede registrarse.
|
||||||
# (Si no hay ningún usuario, el primero siempre puede registrarse.)
|
- ALLOW_SIGNUP=${ALLOW_SIGNUP:-false}
|
||||||
- ALLOW_SIGNUP=${ALLOW_SIGNUP:-true}
|
|
||||||
volumes:
|
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
|
- ./data:/app/data
|
||||||
# Mangas descargados: <Título>/Capitulo NNNN.cbz + cover.jpg
|
# Mangas descargados: <Título>/Capitulo NNNN.cbz + cover.jpg
|
||||||
- /zdata/media/mangas:/mangas
|
- ${MANGA_DIR_HOST:-./mangas}:/mangas
|
||||||
ports:
|
ports:
|
||||||
# Local del servidor, LAN y Tailscale.
|
# 0.0.0.0 no depende de que la IP de la LAN o de Tailscale existan al arrancar.
|
||||||
- "127.0.0.1:3352:3352"
|
# Para limitarlo a una interfaz: BIND_IP=192.168.1.141 en .env.
|
||||||
- "192.168.1.141:3352:3352"
|
- "${BIND_IP:-0.0.0.0}:${HOST_PORT:-3352}:3352"
|
||||||
- "${IP_TAILSCALE:-100.125.134.51}:3352:3352"
|
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp
|
- /tmp
|
||||||
|
|||||||
Reference in New Issue
Block a user