LibreLedger is a single-person money ledger. The browser encrypts
everything (PBKDF2-HMAC-SHA256 with 250k iterations, then AES-256-GCM),
and a small standard-library Python server stores only the resulting
{v, salt, iv, ct} blob. Ledger files from the earlier Money Ledger version
open unchanged.
- On plain http to a LAN or VPN address, where browsers hide Web Crypto,
the app switches to vendored @noble/hashes and @noble/ciphers 2.2.0 and
says so on the lock screen. tests/crypto-interop.test.mjs shows both
paths read each other's files.
- The server hands out only the app's own files, sends a self-only CSP,
nosniff, frame denial and no-referrer, checks the shape of each blob,
refuses cross-site writes and writes atomically. It keeps the last 10
backups plus one per day for 30 days.
- The container runs that server from a digest-pinned python alpine
image, as an unprivileged user, with its data in /data and a health
check on /api/health.
Assisted-by: Claude Opus 5 <noreply@anthropic.com>
45 lines
2.0 KiB
Docker
45 lines
2.0 KiB
Docker
# LibreLedger — the app plus its persistence server (server.py, standard
|
|
# library only). The browser encrypts; the container only stores ciphertext.
|
|
#
|
|
# Build: docker build -t libreledger .
|
|
# Run: docker run --rm -p 8080:8080 -v libreledger-data:/data libreledger
|
|
# Open: http://localhost:8080
|
|
#
|
|
# Base image pinned by version AND index digest (multi-arch). Bump both together.
|
|
FROM python:3.14.7-alpine3.24@sha256:c6ead215bfd31f1e433d968853b7a769989117115b728874824e6c0a27cb96fc
|
|
|
|
ARG VERSION=1.0.0
|
|
LABEL org.opencontainers.image.title="LibreLedger" \
|
|
org.opencontainers.image.description="Encrypted personal money ledger: your data is encrypted in your browser; the server never sees it." \
|
|
org.opencontainers.image.version="${VERSION}" \
|
|
org.opencontainers.image.source="https://git.libreportal.org/LibrePortal/LibreLedger" \
|
|
org.opencontainers.image.licenses="AGPL-3.0-only"
|
|
|
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
PYTHONUNBUFFERED=1 \
|
|
LIBRELEDGER_HOST=0.0.0.0 \
|
|
LIBRELEDGER_PORT=8080 \
|
|
LIBRELEDGER_DATA_DIR=/data
|
|
|
|
# Unprivileged by default. LibrePortal may override the uid so the process
|
|
# owns its bind mount (under rootless Docker that is container root, which is
|
|
# the unprivileged install user on the host).
|
|
RUN addgroup -S -g 10001 libreledger \
|
|
&& adduser -S -D -H -u 10001 -G libreledger -s /sbin/nologin libreledger \
|
|
&& mkdir -p /data \
|
|
&& chown libreledger:libreledger /data \
|
|
&& chmod 700 /data
|
|
|
|
WORKDIR /app
|
|
COPY --chmod=0444 server.py index.html app.js theme.js crypto-fallback.js styles.css favicon.svg ./
|
|
COPY --chmod=0444 banks/*.svg ./banks/
|
|
COPY --chmod=0444 vendor/ ./vendor/
|
|
RUN find /app -type d -exec chmod 0555 {} +
|
|
|
|
USER libreledger
|
|
EXPOSE 8080
|
|
VOLUME ["/data"]
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
CMD ["python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/api/health', timeout=4).status == 200 else 1)"]
|
|
CMD ["python3", "/app/server.py"]
|