# Grimmory v3.5.0 — upstream source READ over HTTPS (raw.githubusercontent.com at the tag), 2026-10-02T06:28:57Z. No box touched.

## 1.7 the image's start — Dockerfile (runtime stage) and packaging/docker/entrypoint.sh
FROM eclipse-temurin:25-jre-alpine

ENV JAVA_TOOL_OPTIONS="-XX:+UseShenandoahGC \
    -XX:ShenandoahGCHeuristics=compact \
    -XX:+UseCompactObjectHeaders \
    -XX:MaxRAMPercentage=60.0 \
    -XX:InitialRAMPercentage=8.0 \
    -XX:+ExitOnOutOfMemoryError \
    -XX:MaxMetaspaceSize=256m \
    -XX:ReservedCodeCacheSize=48m \
    -Xss512k \
    -XX:CICompilerCount=2 \
    -XX:+UnlockExperimentalVMOptions \
    -XX:+UseStringDeduplication \
    -XX:ShenandoahUncommitDelay=5000 \
    -XX:ShenandoahGuaranteedGCInterval=30000 \
    -XX:MaxDirectMemorySize=256m \
    --enable-native-access=ALL-UNNAMED \
    --enable-preview"

RUN apk add --no-cache su-exec libstdc++ libgcc libarchive && \
    mkdir -p /bookdrop

# Manually link `libarchive.so.13` so java and other libraries can see it
RUN ln -s /usr/lib/libarchive.so.13 /usr/lib/libarchive.so

COPY packaging/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

COPY --from=ffprobe-layer /ffprobe /usr/local/bin/ffprobe
COPY --from=kepubify-layer /kepubify /usr/local/bin/kepubify

COPY --from=backend-build /workspace/backend/app.jar /app/app.jar

ARG APP_VERSION=development
ARG APP_REVISION=unknown

LABEL org.opencontainers.image.title="Grimmory" \
    org.opencontainers.image.description="Grimmory: a self-hosted, multi-user digital library with smart shelves, auto metadata, Kobo and KOReader sync, BookDrop imports, OPDS support, and a built-in reader for EPUB, PDF, and comics." \
    org.opencontainers.image.source="https://github.com/grimmory-tools/grimmory" \
    org.opencontainers.image.url="https://github.com/grimmory-tools/grimmory" \
    org.opencontainers.image.documentation="https://grimmory.org/docs/getting-started" \
    org.opencontainers.image.version=$APP_VERSION \
    org.opencontainers.image.revision=$APP_REVISION \
    org.opencontainers.image.licenses="AGPL-3.0" \
    org.opencontainers.image.base.name="docker.io/library/eclipse-temurin:25-jre-alpine"

ENV APP_VERSION=${APP_VERSION} \
    APP_REVISION=${APP_REVISION}

EXPOSE 6060

HEALTHCHECK --interval=60s --timeout=10s --start-period=60s --retries=5 \
  CMD wget -q --spider http://localhost:${SERVER_PORT:-${BOOKLORE_PORT:-6060}}/api/v1/healthcheck

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["java", "--enable-native-access=ALL-UNNAMED", "--enable-preview", "-jar", "/app/app.jar"]
=== backend/src/main/resources/application.yaml
=== packaging/docker/entrypoint.sh
#!/bin/sh
set -e

USER_ID="${USER_ID:-1000}"
GROUP_ID="${GROUP_ID:-1000}"
APP_USER="${APP_USER:-booklore}"

if getent group "$APP_USER" >/dev/null 2>&1; then
    existing_group_id="$(getent group "$APP_USER" | cut -d: -f3)"
    if [ "$existing_group_id" != "$GROUP_ID" ]; then
        echo "ERROR: APP_USER group '$APP_USER' already exists with GID $existing_group_id, expected $GROUP_ID." >&2
        exit 1
    fi
fi

# Create group and user if they don't exist
if ! getent group "$GROUP_ID" >/dev/null 2>&1; then
    addgroup -g "$GROUP_ID" -S "$APP_USER"
fi

if getent passwd "$APP_USER" >/dev/null 2>&1; then
    existing_user_id="$(getent passwd "$APP_USER" | cut -d: -f3)"
    if [ "$existing_user_id" != "$USER_ID" ]; then
        echo "ERROR: APP_USER '$APP_USER' already exists with UID $existing_user_id, expected $USER_ID." >&2
        exit 1
    fi
fi

if ! getent passwd "$USER_ID" >/dev/null 2>&1; then
    adduser -u "$USER_ID" -G "$(getent group "$GROUP_ID" | cut -d: -f1)" -S -D "$APP_USER"
fi

# Ensure data, bookdrop, and books directories exist and are writable by the target user
mkdir -p /app/data /bookdrop /books
chown "$USER_ID:$GROUP_ID" /app/data /bookdrop /books 2>/dev/null || true

exec su-exec "$USER_ID:$GROUP_ID" "$@"

## 1.6 / 1.8 / 0.4 — every env placeholder the app's settings read (backend/src/main/resources/application.yaml)
3:  path-config: ${APP_PATH_CONFIG:/app/data}
4:  bookdrop-folder: ${APP_BOOKDROP_FOLDER:/bookdrop}
6:    enabled: ${API_DOCS_ENABLED:false}
12:    allowed-origins: ${ALLOWED_ORIGINS:*}
15:    enabled: ${REMOTE_AUTH_ENABLED:false}
16:    create-new-users: ${REMOTE_AUTH_CREATE_NEW_USERS:true}
17:    header-name: ${REMOTE_AUTH_HEADER_NAME:Remote-Name}
18:    header-user: ${REMOTE_AUTH_HEADER_USER:Remote-User}
19:    header-email: ${REMOTE_AUTH_HEADER_EMAIL:Remote-Email}
20:    header-groups: ${REMOTE_AUTH_HEADER_GROUPS:Remote-Groups}
21:    admin-group: ${REMOTE_AUTH_ADMIN_GROUP}
22:    groups-delimiter: ${REMOTE_AUTH_GROUPS_DELIMITER:\\s+}
23:  disk-type: ${DISK_TYPE:LOCAL}
26:    force-disable: ${FORCE_DISABLE_OIDC:false}
27:    allow-unsafe-hosts: ${OIDC_ALLOW_UNSAFE_HOSTS:false} # If enabled, turn off outbound SSRF filtering
65:  port: ${SERVER_PORT:${BOOKLORE_PORT:6060}}
97:    url: ${DATABASE_URL:jdbc:mariadb://${DATABASE_HOST:${DB_HOST:mariadb}}:${DATABASE_PORT:3306}/${DATABASE_NAME:booklore}?createDatabaseIfNotExist=true&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true}
98:    username: ${DATABASE_USERNAME:root}
99:    password: ${DATABASE_PASSWORD:${MYSQL_ROOT_PASSWORD}}
132:        log_slow_query: ${HIBERNATE_SLOW_QUERY_MS:500}  # Log queries taking longer than 500ms (see org.hibernate.SQL_SLOW)
133:        generate_statistics: ${HIBERNATE_STATISTICS:false}
143:    enabled: ${app.api-docs.enabled:false}
148:    root: ${ROOT_LOG_LEVEL:INFO}
149:    org.booklore: ${LOG_LEVEL:INFO}
154:    org.hibernate.session.metrics: ${HIBERNATE_METRICS_LOG_LEVEL:WARN}

## 1.2 upstream compose (deploy/compose/docker-compose.yml) — the database it runs
8:    image: ghcr.io/grimmory-tools/grimmory:latest
23:      # Override JVM memory defaults (the image ships sensible defaults for ~512 MB heap).
25:      # - JDK_JAVA_OPTIONS=-Xmx256m
28:      # - JDK_JAVA_OPTIONS=-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/data
52:    image: lscr.io/linuxserver/mariadb:11.4.8

## 1.9 the token-signing key — service/security/JwtSecretService.java (generated at first start, stored in the DB)
5:import org.booklore.repository.JwtSecretRepository;
9:import java.security.SecureRandom;
14:public class JwtSecretService {
31:                            .orElseGet(this::generateAndStoreNewSecret);
40:    private String generateAndStoreNewSecret() {
41:        String newSecret = generateRandomSecret();
43:        jwtSecretRepository.save(secretEntity);
47:    private String generateRandomSecret() {
49:        new SecureRandom().nextBytes(randomBytes);

## 0.4 the version check — service/VersionService.java
77:    private static final String GITHUB_REPO = "grimmory-tools/grimmory";
78:    private static final String BASE_URI = "https://api.github.com/repos/" + GITHUB_REPO;
104:            latest = fetchLatestGitHubReleaseVersion();
117:    public String fetchLatestGitHubReleaseVersion() {
120:                    .uri(BASE_URI + "/releases/latest")
143:                    .uri(BASE_URI + "/releases?per_page=" + MAX_RELEASES)

## 3.6 / 3.10 the sign-in lock — config/security/service/AuthRateLimitService.java + AuthenticationService.java
206:    private static final int MAX_ATTEMPTS = 5;
215:                .expireAfterWrite(Duration.ofMinutes(15))
222:        checkRateLimit("login:ip:" + ip, AuditAction.LOGIN_RATE_LIMITED, "Login rate limited for IP: " + ip);
227:        checkRateLimit("login:user:" + normalizedUsername, AuditAction.LOGIN_RATE_LIMITED, "Login rate limited for username: " + normalizedUsername);
231:        recordFailedAttempt("login:ip:" + ip);
236:        recordFailedAttempt("login:user:" + normalizedUsername);
240:        resetAttempts("login:ip:" + ip);
245:        resetAttempts("login:user:" + normalizedUsername);
141:        String ip = RequestUtils.getCurrentRequest().getRemoteAddr();
143:        authRateLimitService.checkLoginRateLimit(ip);
144:        authRateLimitService.checkLoginRateLimitByUsername(username);
221:        String ip = RequestUtils.getCurrentRequest().getRemoteAddr();
64:  forward-headers-strategy: native
(Spring Boot 'native' = Tomcat RemoteIpValve: X-Forwarded-For read from the RIGHT, skipping internal proxies incl. 172.16.0.0/12 — the 2026-10-01 sweep, felhom.eu/documentation/audits/visitors-2026-10-01/A/sweep/sweep-4.md)

## 7.1 mail — the app's own e-mail providers (send a book by e-mail) are set in its UI, stored in its DB; no env and no mail at start
backend/src/main/java/org/booklore/controller/EmailProviderV2Controller.java
backend/src/main/java/org/booklore/controller/EmailRecipientV2Controller.java
backend/src/main/java/org/booklore/controller/SendEmailV2Controller.java
backend/src/main/java/org/booklore/mapper/EmailProviderV2Mapper.java
backend/src/main/java/org/booklore/mapper/EmailRecipientV2Mapper.java
backend/src/main/java/org/booklore/model/dto/EmailProviderV2.java
backend/src/main/java/org/booklore/model/dto/EmailRecipientV2.java
backend/src/main/java/org/booklore/model/dto/request/CreateEmailProviderRequest.java
backend/src/main/java/org/booklore/model/dto/request/CreateEmailRecipientRequest.java
backend/src/main/java/org/booklore/model/dto/request/SendBookByEmailRequest.java
backend/src/main/java/org/booklore/model/entity/EmailProviderV2Entity.java
backend/src/main/java/org/booklore/model/entity/EmailRecipientV2Entity.java
backend/src/main/java/org/booklore/model/entity/UserEmailProviderPreferenceEntity.java
backend/src/main/java/org/booklore/repository/EmailProviderV2Repository.java
backend/src/main/java/org/booklore/repository/EmailRecipientV2Repository.java
backend/src/main/java/org/booklore/repository/UserEmailProviderPreferenceRepository.java
backend/src/main/java/org/booklore/service/email/EmailProviderV2Service.java
backend/src/main/java/org/booklore/service/email/EmailRecipientV2Service.java
backend/src/main/java/org/booklore/service/email/SendEmailV2Service.java
