This is an archive for myself. An archive of my own, so to speak.
Self-hosted, git-backed reading site for Markdown-formatted creative writing with YAML frontmatter. Written so I can share my writing with my friends so I don't have to compete with all the other writers on AO3 writing fanfiction while I'm writing wholly original wish-fufillment.
Update public stories over HTTP with git. Read through your revision history for a story with immersive, unified, and split diff viewers. Have control over visibilty of stories, authorship, and granular suppression of history. Export your stories in PDF format using Pandoc and Typst, or alternatively create the export format of your choosing with your own Python module.
- uv (installs Python 3.13 and the dependencies for you)
- Optional: Caddy (or another reverse proxy) for HTTPS and rate limiting
If you want your friends to be able to download your stories as PDFs, you can point the PDF_SCRIPTS variable at a directory of Python builder modules (see below for module schema). This repository includes default download modules in pdf-scripts/. Their Python dependencies are the pdf extra (uv sync --extra pdf).
This project supports two deployment types. I do a bare metal install, so that's the one I will personally support the most. The other is a containterized install for Docker and Podman.
This project contains a script that can deploy LibraryOfMyOwn on a Debian Linux system (specifically intended for a Raspberry Pi 4B on Raspbian Trixie, but it should work on any Debian Linux or Debian derivative like Ubuntu Server).
The deployment script is fairly self-contained, installing the project itself and its dependencies under /opt/libmyown. The script auto-detects CPU architecture and downloads matching Typst 0.15.1 and Pandoc 3.12 binaries into /opt/libmyown/bin, creates a system libmyown user, and enables the libmyown systemd unit.
Fresh system install:
curl -fsSL https://raw.githubusercontent.com/ganyuke/LibraryOfMyOwn/main/scripts/deploy-pi.sh | sudo bashAlready cloned / Updating existing instance:
sudo ./scripts/deploy-pi.shRe-running the script runs git pull in /opt/libmyown/app and refreshes dependencies. Your data under /opt/libmyown/data is preserved, and the running site is restarted to load the update. If Git complains about "dubious ownership", ensure that you run the command with su -u libmyown.
Outdated versions of the script will try to fetch the latest version with a user prompt. Add -y to answer yes automatically.
Older versions of the script may emit the error: missing requirements.txt. The script will still pull down the latest verison of the repository, so you should be able to just retry running the script again.
Your startup configuration can be done through environment variables (.env). The minimum required variables are DATA_DIR, HOST, and PORT. Secrets other than the admin password are autogenerated when unspecified. Secrets are stored in data/secrets.json. Please keep this file safe.
On first start, the service prints a generated admin password to its log (sudo journalctl -u libmyown | grep 'Generated admin password'). Log in and change it under Admin → Security, or set ADMIN_PASSWORD in .env before the first start to choose your own.
Edit /opt/libmyown/app/.env (from examples/env.example), start the service, then log in at /login.
You can deploy using Docker on amd64 and arm64 via this project's GitHub containers. PDF downloads work out of the box. You will need to set up a reverse proxy separately.
docker run -d --name libmyown -p 127.0.0.1:4033:4033 \
-v libmyown-data:/data \
--env-file .env \
ghcr.io/ganyuke/libraryofmyown:latest
docker logs libmyown 2>&1 | grep 'Generated admin password'Caution
Make sure you pass in a volume or you will lose your data on restart!
To update, pull the new image and recreate the container:
docker pull ghcr.io/ganyuke/libraryofmyown:latest
docker rm -f libmyown
# then run the same `docker run` command as beforeYou can also deploy this project through Docker Compose, using examples/docker/compose.yaml. Replace your.domain with, well, your domain, in both compose.yaml and Caddyfile, then run docker compose up -d. The compose file includes Caddy with rateliimting by default, which fetches TLS certificates for you and rate limits logins, git access and PDF downloads. The first start takes a few minutes while Caddy is built.
This project does not support HTTPS or ratelimiting out of the box. You will need to supply this functionality yourself. There are many off-the-shelf options that can do this for you. This project supplies examples that can get you started with Caddy as your middleman.
An example Caddyfile configuration can be found at examples/caddy/Caddyfile. This configuration offers both TLS and per-IP rate limits on /login and /git/*. However, this setup requires a custom Caddy build with caddy-ratelimit (Caddy does not support ratelimits out of the box):
See examples/caddy/README.md for details on how to build Caddy for use with this specific Caddyfile configuration.
If you choose not to use Caddy, ensure your reverse proxy forwards the header X-Forwarded-Proto. If the proxy runs on another machine, add its address to TRUSTED_PROXIES in .env.
If PUBLIC_URL is https:// and you want to log in over plain http:// (e.g. local testing), set HTTPS_ENABLED=false.
This project does not do automated backups, so you'll have to do them yourself. The most important files that you probably want to keep are the following:
/opt/libmyown/data/stories.git: your stories and their history. You can also re-push from your writing repo, but if you've rebased or amended since, any history you hid will show up again./opt/libmyown/data/site.json: everything you set in the admin panel, including hidden revisions/opt/libmyown/data/secrets.json: your passwords. Keep it private!/opt/libmyown/app/.env: if you edited it
Everything else in the data directory rebuilds itself.
With Docker, the same files are in the /data volume. You can copy them out with docker cp libmyown:/data ./libmyown-backup.
To restore, run the deploy script on the new machine, stop the service, copy the files back into /opt/libmyown/data, chown them to libmyown, and start it again.
uv run python -m libmyown.mainThe app reads .env from the project root on its own.
Open http://127.0.0.1:4033 and log in at /login with your ADMIN_PASSWORD, or with the generated password printed in the terminal on first start.
Caution
Do not run the script below on a production install. You will lose everything.
This repository includes a sample dataset that you can load with:
uv run scripts/seed_sample.pyThe script imports the Markdown files from the variable STORIES_SOURCE (default: fixtures/sample-stories), adds a few sample commits for history/compare, and publishes the path Series/ by default.
You can run checks with:
uv run pytest
uv run scripts/smoke_test.pyThis section details the workflows that this application expects you to endure!
After logging in, open Admin → Site settings for the git remote URL and Admin → Security for the git push password.
On your writing machine, you will want to create a git repository (if you do not have one already) and add the git remote for your Library Of My Own install.
cd $YOUR_WRITING_MATERIALS
git init
git remote add library https://git@your.domain/git/stories.git
# Add your .md files, commit, then:
git push -u library mainGit asks for the password on the first push. To have it remembered in your system keyring instead of retyping it (and instead of putting it in the remote URL, where it sits in plain text in .git/config):
# Fedora
sudo dnf install git-credential-libsecret
git config --global credential.helper libsecretOn macOS, git uses the Keychain by default. On Windows, Git Credential Manager (bundled with Git for Windows) does the same.
New pushes show up on the site a few seconds later. The repository tidies itself up automatically, or you can do it yourself from Admin → Maintenance.
Pick the branch the site shows under Admin → Site settings → Published branch (by default, the branch you pushed first). The site shows stories EXCLUSIVELY from that branch, so you can keep drafts on other branches. If the published branch is ever missing, the site shows nothing until you push it or pick another.
Your root index page will, by default, show NOTHING. This is to be expected! All stories by default are hidden from view. To make them readable to the public (i.e. let your friends and their AI friends and their AI's friends read it), you need to publish those stories through the admin panel.
- Log in with your admin password at
/login - Go to Admin → Publish works
- Select files or folders to expose
In Admin → Authorship, you can configure who the author of a particular piece appears as. By default, this is the site author, configured in the same panel. You can choose the author to appear as the author of the earliest Git commit, override the author completely with an arbitrary name, and mask Git author names with display names.
So even if you committed to your writing repo with an embarassing name that you can't change, you don't have to let anyone see it!
All works are expected to have a title field used for the work's heading. The work's slug is derived from its file name. The site also supports specific handling for the characters field, a YAML list of <character name>: <character description> entries. The description for the story
Apart from those fields, you can put any string fields in your Markdown frontmatter. All other frontmatter fields appear on the work page as a bold label and description pair.
In Admin → Work metadata, you can configure:
- Blurb field names: ordered list of frontmatter keys to try for the large summary blurb under the title (default:
summary) - Metadata field order: display order for the metadata list; any other fields sort alphabetically after these
The panel also lists frontmatter keys it has seen across your published works.
In Admin → Site settings, you can set the site title shown in the header, public URL, privacy toggles, git username, and stories branch.
This project supports downloading Markdown files as PDFs. By default, this project supports the following options for export:
| Script | Description |
|---|---|
digital |
Quarter-letter, symmetric margins for screen reading |
logical |
Quarter-letter reading order with print gutter margins |
cutstack |
US Letter cut-and-stack imposition (requires pdfimpose) |
onecut |
US Letter one-cut fold imposition (requires pypdf) |
Digital is intended for (as its name suggests) digital consumption while the rest are optimized for printing on US Letter-sized paper. All four split the story into 4-up on US Letter.
Pandoc and Typst binaries must be on PATH for these options to function. The deploy script installs everything needed. For a local checkout, run uv sync --extra pdf.
This project supports adding your own export options. Your custom export option must be a Python module that defines the following:
label: button text shown on the work pagebuild(input_md, output_pdf, work_dir, *, work=None, author="", rev_label="", blurb_fields=None, title="", work_path=""): write a PDF tooutput_pdf. LibraryOfMyOwn parses the work once and passes structuredwork(title,fields,characters,body). Legacy builders that only readinput_mdstill work, but built-in scripts expectwork.suffix(optional): appended to the work filename for the download, e.g."-digital-quarter-letter.pdf". Defaults to-{script_id}.pdf.order(optional): sort position on the work page (lower first). Defaults to0.
An example minimal builder lives at examples/pdf-scripts/plain.py.
The Python module's filename (without .py) becomes the URL slug: /works/{slug}/pdf/{id}.
Python modules with filenames that start with _ are ignored.
Builds taking longer than PDF_BUILD_TIMEOUT seconds (default 180) are stopped.
By default the app loads Python modules from pdf-scripts/ in the repo (configurable by specifying a different path with PDF_SCRIPTS in .env). If PDF_SCRIPTS is unset and pdf-scripts/ is missing, the Download row is hidden.