Trending-skills grimmory-self-hosted-library
Expert knowledge for setting up, configuring, and extending Grimmory — a self-hosted book library manager supporting EPUBs, PDFs, comics, Kobo sync, OPDS, and multi-user management.
git clone https://github.com/Aradotso/trending-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/grimmory-self-hosted-library" ~/.claude/skills/aradotso-trending-skills-grimmory-self-hosted-library && rm -rf "$T"
skills/grimmory-self-hosted-library/SKILL.mdGrimmory Self-Hosted Library Manager
Skill by ara.so — Daily 2026 Skills collection.
Grimmory is a self-hosted application (successor to BookLore) for managing your entire book collection. It supports EPUBs, PDFs, MOBIs, AZW/AZW3, and comics (CBZ/CBR/CB7), with a built-in browser reader, annotations, Kobo/OPDS sync, KOReader progress sync, metadata enrichment, and multi-user support.
Installation
Requirements
- Docker and Docker Compose
Step 1: Create .env
.env# Application APP_USER_ID=1000 APP_GROUP_ID=1000 TZ=Etc/UTC # Database DATABASE_URL=jdbc:mariadb://mariadb:3306/grimmory DB_USER=grimmory DB_PASSWORD=${DB_PASSWORD} # Storage: LOCAL (default) or NETWORK DISK_TYPE=LOCAL # MariaDB DB_USER_ID=1000 DB_GROUP_ID=1000 MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE=grimmory
Step 2: Create docker-compose.yml
docker-compose.ymlservices: grimmory: image: grimmory/grimmory:latest # Alternative registry: ghcr.io/grimmory-tools/grimmory:latest container_name: grimmory environment: - USER_ID=${APP_USER_ID} - GROUP_ID=${APP_GROUP_ID} - TZ=${TZ} - DATABASE_URL=${DATABASE_URL} - DATABASE_USERNAME=${DB_USER} - DATABASE_PASSWORD=${DB_PASSWORD} - DISK_TYPE=${DISK_TYPE} depends_on: mariadb: condition: service_healthy ports: - "6060:6060" volumes: - ./data:/app/data - ./books:/books - ./bookdrop:/bookdrop healthcheck: test: wget -q -O - http://localhost:6060/api/v1/healthcheck interval: 60s retries: 5 start_period: 60s timeout: 10s restart: unless-stopped mariadb: image: lscr.io/linuxserver/mariadb:11.4.5 container_name: mariadb environment: - PUID=${DB_USER_ID} - PGID=${DB_GROUP_ID} - TZ=${TZ} - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE=${MYSQL_DATABASE} - MYSQL_USER=${DB_USER} - MYSQL_PASSWORD=${DB_PASSWORD} volumes: - ./mariadb/config:/config restart: unless-stopped healthcheck: test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"] interval: 5s timeout: 5s retries: 10
Step 3: Launch
docker compose up -d # View logs docker compose logs -f grimmory # Check health curl http://localhost:6060/api/v1/healthcheck
Open http://localhost:6060 and create your admin account.
Volume Layout
./data/ # App data, thumbnails, user config ./books/ # Your book files (mounted at /books) ./bookdrop/ # Drop-zone for auto-import (mounted at /bookdrop) ./mariadb/ # MariaDB data
Environment Variables Reference
| Variable | Description | Default |
|---|---|---|
| UID for the app process | |
| GID for the app process | |
| Timezone string | |
| JDBC connection string | required |
| DB username | required |
| DB password | required |
| or | |
Supported Book Formats
| Category | Formats |
|---|---|
| eBooks | EPUB, MOBI, AZW, AZW3 |
| Documents | |
| Comics | CBZ, CBR, CB7 |
BookDrop (Auto-Import)
Drop files into
./bookdrop/ on your host. Grimmory watches the folder, extracts metadata from Google Books and Open Library, and queues books for review.
./bookdrop/ my-novel.epub ← dropped here another-book.pdf ← dropped here
Flow:
- Watch — Grimmory monitors
continuously/bookdrop - Detect — New files are picked up and parsed
- Enrich — Metadata fetched from Google Books / Open Library
- Import — Review in UI, adjust if needed, confirm import
Volume mapping required in
docker-compose.yml:
volumes: - ./bookdrop:/bookdrop
Network Storage Mode
For NFS, SMB, or other network-mounted filesystems, set
DISK_TYPE=NETWORK. This disables destructive UI operations (delete, move, rename) to protect shared mounts while keeping reading, metadata, and sync fully functional.
# .env DISK_TYPE=NETWORK
Java Backend — Key Patterns
Grimmory is a Java application (Spring Boot + MariaDB). When contributing or extending:
Project Structure (typical Spring Boot layout)
src/main/java/ com/grimmory/ config/ # Spring configuration classes controller/ # REST API controllers service/ # Business logic repository/ # JPA repositories model/ # JPA entities dto/ # Data transfer objects
REST API — Base Path
All endpoints are under
/api/v1/:
# Health check GET http://localhost:6060/api/v1/healthcheck # Books GET http://localhost:6060/api/v1/books GET http://localhost:6060/api/v1/books/{id} POST http://localhost:6060/api/v1/books PUT http://localhost:6060/api/v1/books/{id} DELETE http://localhost:6060/api/v1/books/{id} # Shelves GET http://localhost:6060/api/v1/shelves POST http://localhost:6060/api/v1/shelves # OPDS catalog (for compatible reader apps) GET http://localhost:6060/opds
Example: Querying the API with Java (OkHttp)
import okhttp3.*; import com.fasterxml.jackson.databind.ObjectMapper; public class GrimmoryClient { private final OkHttpClient http = new OkHttpClient(); private final ObjectMapper mapper = new ObjectMapper(); private final String baseUrl; private final String token; public GrimmoryClient(String baseUrl, String token) { this.baseUrl = baseUrl; this.token = token; } public String getBooks() throws Exception { Request request = new Request.Builder() .url(baseUrl + "/api/v1/books") .header("Authorization", "Bearer " + token) .build(); try (Response response = http.newCall(request).execute()) { return response.body().string(); } } }
Example: Spring Boot Controller Pattern
@RestController @RequestMapping("/api/v1/books") @RequiredArgsConstructor public class BookController { private final BookService bookService; @GetMapping public ResponseEntity<Page<BookDto>> getAllBooks( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String search) { return ResponseEntity.ok(bookService.findAll(page, size, search)); } @GetMapping("/{id}") public ResponseEntity<BookDto> getBook(@PathVariable Long id) { return ResponseEntity.ok(bookService.findById(id)); } @PostMapping public ResponseEntity<BookDto> createBook(@RequestBody @Valid CreateBookRequest request) { return ResponseEntity.status(HttpStatus.CREATED) .body(bookService.create(request)); } @PutMapping("/{id}/metadata") public ResponseEntity<BookDto> updateMetadata( @PathVariable Long id, @RequestBody @Valid UpdateMetadataRequest request) { return ResponseEntity.ok(bookService.updateMetadata(id, request)); } }
Example: JPA Entity Pattern
@Entity @Table(name = "books") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String title; private String author; private String isbn; private String format; // EPUB, PDF, CBZ, etc. @Column(name = "file_path") private String filePath; @Column(name = "cover_path") private String coverPath; @Column(name = "reading_progress") private Double readingProgress; @ManyToMany @JoinTable( name = "book_shelf", joinColumns = @JoinColumn(name = "book_id"), inverseJoinColumns = @JoinColumn(name = "shelf_id") ) private Set<Shelf> shelves = new HashSet<>(); @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; }
Example: Service with Metadata Enrichment
@Service @RequiredArgsConstructor public class MetadataService { private final GoogleBooksClient googleBooksClient; private final OpenLibraryClient openLibraryClient; private final BookRepository bookRepository; public BookDto enrichMetadata(Long bookId) { Book book = bookRepository.findById(bookId) .orElseThrow(() -> new BookNotFoundException(bookId)); // Try Google Books first Optional<BookMetadata> metadata = googleBooksClient.search(book.getTitle(), book.getAuthor()); // Fall back to Open Library if (metadata.isEmpty()) { metadata = openLibraryClient.search(book.getIsbn()); } metadata.ifPresent(m -> { book.setDescription(m.getDescription()); book.setCoverUrl(m.getCoverUrl()); book.setPublisher(m.getPublisher()); book.setPublishedDate(m.getPublishedDate()); bookRepository.save(book); }); return BookDto.from(book); } }
OPDS Integration
Connect any OPDS-compatible reader app (Kybook, Chunky, Moon+ Reader, etc.) using:
http://<your-host>:6060/opds
Authenticate with your Grimmory username and password when prompted.
Kobo / KOReader Sync
- Kobo: Connect via the device sync feature in Grimmory settings. The app exposes a sync endpoint compatible with Kobo's API.
- KOReader: Configure KOReader's sync plugin to point to your Grimmory instance URL.
Multi-User & Authentication
Local Authentication
Create users from the admin panel at http://localhost:6060. Each user has isolated shelves, reading progress, and preferences.
OIDC Authentication
Configure via environment variables (refer to full documentation at https://grimmory.org/docs/getting-started for OIDC-specific variables such as
OIDC_ISSUER_URI, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET).
Building from Source
# Clone the repository git clone https://github.com/grimmory-tools/grimmory.git cd grimmory # Build with Maven ./mvnw clean package -DskipTests # Or build Docker image locally docker build -t grimmory:local . # Use local build in docker-compose.yml # Comment out 'image' and uncomment 'build: .'
Common Docker Commands
# Start services docker compose up -d # Stop services docker compose down # View app logs docker compose logs -f grimmory # View DB logs docker compose logs -f mariadb # Restart only the app docker compose restart grimmory # Pull latest image and redeploy docker compose pull && docker compose up -d # Open a shell inside the container docker exec -it grimmory /bin/bash # Database shell docker exec -it mariadb mariadb -u grimmory -p grimmory
Troubleshooting
Container won't start — DB connection refused
# Check MariaDB health docker compose ps mariadb # Should show "healthy". If not: docker compose logs mariadb # Ensure DATABASE_URL host matches the service name: mariadb:3306
Books not appearing after BookDrop
# Verify file permissions — UID/GID must match APP_USER_ID/APP_GROUP_ID ls -la ./bookdrop/ # Check app logs for detection events docker compose logs -f grimmory | grep -i bookdrop
Permission denied on ./books or ./data
# Set ownership to match APP_USER_ID / APP_GROUP_ID sudo chown -R 1000:1000 ./books ./data ./bookdrop
OPDS not accessible from reader app
# Confirm port 6060 is reachable from your device curl http://<host-ip>:6060/api/v1/healthcheck # Check firewall rules if on a remote server
High memory usage
MariaDB and Grimmory together require at minimum ~512 MB RAM. For large libraries (10k+ books), allocate 1–2 GB.
Metadata not enriching
Google Books and Open Library require outbound internet access from the container. Verify DNS and network:
docker exec -it grimmory curl -s "https://www.googleapis.com/books/v1/volumes?q=test"
Contributing
Before opening a pull request:
- Open an issue and get maintainer approval
- Include screenshots/video proof and pasted test output
- Follow backend and frontend conventions in
CONTRIBUTING.md - AI-assisted code is allowed but you must run, test, and understand every line
# Run tests before submitting ./mvnw test # Check code style ./mvnw checkstyle:check
Links
- GitHub: https://github.com/grimmory-tools/grimmory
- Docker Hub: https://hub.docker.com/r/grimmory/grimmory
- GHCR:
ghcr.io/grimmory-tools/grimmory - Discord: https://discord.gg/FwqHeFWk
- Docs: https://grimmory.org/docs/getting-started
- License: AGPL-3.0