diff --git a/.gitignore b/.gitignore index 66e6c59..ac229a0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,12 @@ !*/.env.example !*/README.md +# Support files a stack needs at runtime: init scripts, config fragments. +# Keep them in a per-service `init/` directory so they are never mistaken for +# application state. +!*/init/ +!*/init/** + # Docs are not service data. !docs/** diff --git a/README.md b/README.md index 44db654..2964371 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ handled separately — see [Exposing services](#exposing-services). | [cloudflare-tunnel](cloudflare-tunnel/) | — | Outbound connector. Exposes other services without opening a port | | [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery | | [openwebui](openwebui/) | 3033 | LLM chat UI. One `OWUI_TAG` switches between CPU, CUDA and bundled-Ollama | -| [overleaf](overleaf/) | 8020 | LaTeX editor. Needs Mongo + Redis | +| [overleaf](overleaf/) | 8020 | LaTeX editor. Four containers: app, Mongo (replica set), Redis, init | diff --git a/overleaf/.env.example b/overleaf/.env.example new file mode 100644 index 0000000..3a470aa --- /dev/null +++ b/overleaf/.env.example @@ -0,0 +1,32 @@ +# Copy to .env and adjust. .env is git-ignored; this file is not. + +# ─── Required ─────────────────────────────────────────────────────── +# Signs project invite tokens. Generate your own -- do not reuse anyone's: +# +# openssl rand -base64 32 +# +OVERLEAF_INVITE_TOKEN_SECRET= + +# ─── Public address ───────────────────────────────────────────────── +# Every generated link is built from this: account activation, password +# reset, project invites. Overleaf defaults to http://localhost, so links +# come out wrong the moment you use a port or a domain -- and you only find +# out when someone can't reset their password. +# +# local, non-standard port http://192.168.1.50:8020 +# behind a reverse proxy https://overleaf.example.com +# +OVERLEAF_SITE_URL=http://localhost:8020 + +# ─── Basics ───────────────────────────────────────────────────────── +#OVERLEAF_APP_NAME=Overleaf +#PORT=8020 + +# Where Mongo, Redis and Overleaf keep their data. Projects live here -- +# this is the directory to back up. +#DATA_PATH=./data + +# ─── HTTPS ────────────────────────────────────────────────────────── +# Serving over TLS? Uncomment OVERLEAF_SECURE_COOKIE in compose.yaml. +# Careful: the code checks `!= null`, so setting it to "false" turns it ON. +# The only way to disable it is to leave it unset. diff --git a/overleaf/README.md b/overleaf/README.md new file mode 100644 index 0000000..3c92294 --- /dev/null +++ b/overleaf/README.md @@ -0,0 +1,190 @@ +# Overleaf + +Self-hosted LaTeX editor — the Community Edition of overleaf.com. Real-time +collaborative editing, compiles server-side, keeps a full TeX Live install so +you don't maintain one locally. + +## Quick start + +```bash +cp .env.example .env +echo "OVERLEAF_INVITE_TOKEN_SECRET=$(openssl rand -base64 32)" >> .env +$EDITOR .env # set OVERLEAF_SITE_URL +docker compose up -d +``` + +First boot takes a minute or two. Then create the first admin account — there +is no signup form until a user exists: + +```bash +docker compose exec sharelatex bash -c \ + 'cd /overleaf/services/web && node modules/server-ce-scripts/scripts/create-user.mjs --admin --email=you@example.com' +``` + +It prints an activation URL. Open it, set a password, done. + +## Ports + +| Port | Proto | Purpose | +|------|-------|---------| +| 8020 | http | Web UI (container listens on 80) | + +Mongo and Redis are reachable only on the internal compose network. + +## Why it looks like this + +**Four containers, not one.** The app, MongoDB, Redis, and a one-shot replica-set +initializer. Overleaf is a small distributed system pretending to be an app. + +**Mongo runs as a replica set** (`--replSet overleaf`) even though there's one +node. Overleaf uses transactions and change streams, which Mongo only provides +in replica-set mode. Something has to call `rs.initiate()` exactly once, which +is what [`init/mongodb-init-replica-set.js`](init/mongodb-init-replica-set.js) +does — Mongo runs anything in `/docker-entrypoint-initdb.d/` on first start +against an empty data directory. Skip this and Overleaf hangs at boot with +errors that don't mention replica sets. + +`extra_hosts: mongo:127.0.0.1` is part of the same trick: the init script +registers the member as `mongo:27017`, so during initialization the container +has to resolve its own service name. + +**No `SANDBOXED_COMPILES`.** Upstream's sample `docker-compose.yml` ships these +*enabled*: + +```yaml +SANDBOXED_COMPILES: "true" +DOCKER_RUNNER: "true" +SANDBOXED_COMPILES_SIBLING_CONTAINERS: "true" +``` + +Directly above them, its own comment says sandboxed compiles are Server Pro +only and "must be commented out to avoid compile issues" on Community Edition. +Copy that file as-is and your documents don't compile. They're absent here on +purpose. + +**The image is pinned to `6.2.2`.** Upstream's sample is unpinned. This is a +database-backed service with schema migrations between majors — an unattended +`docker compose pull` picking up a new major is not something you want to +discover on a Sunday. + +**`container_name: overleaf-mongo` / `overleaf-redis`**, while the *service* +names stay `mongo` and `redis`. The service names are load-bearing — +`OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex` resolves them — but upstream's +`container_name: mongo` would claim a very generic name on the host and collide +with the next stack that wants it. + +**`stop_grace_period: 60s`** so in-flight compiles finish instead of being +killed mid-run. + +## Behind your own domain + +Set the public URL — this is the setting people miss: + +```bash +# .env +OVERLEAF_SITE_URL=https://overleaf.example.com +``` + +Then uncomment `OVERLEAF_SECURE_COOKIE` in `compose.yaml`. + +Caddy, which handles WebSockets automatically (Overleaf needs them for +collaborative editing and the compile log): + +```caddy +overleaf.example.com { + reverse_proxy 192.168.1.50:8020 +} +``` + +That's the whole config — Caddy gets the certificate itself. See +[reverse proxy](../docs/reverse-proxy.md). + +A [Cloudflare Tunnel](../cloudflare-tunnel/) also works and passes WebSockets, +but mind the ~100 MB upload cap if you bring in large figures or datasets. + +**Order matters:** change `OVERLEAF_SITE_URL` *before* creating accounts, or the +activation links you've already sent point at the wrong host. + +## Running it on OpenMediaVault + +Two things differ from a plain `docker compose up`, and both cost me an evening. + +**Copy `init/mongodb-init-replica-set.js` across before the first start.** OMV's +compose plugin deploys the compose file and the env file, not the rest of the +directory. If that script isn't on disk when Mongo first starts, Docker creates +it as an empty *directory* — bind mounts do that for missing paths — Mongo skips +it, and the replica set is never initialized. + +The symptom points nowhere near the cause: Mongo reports **healthy** while the +app restart-loops with `500_check_db_access.sh failed with status 1`. Mongo's +healthcheck only runs `db.stats().ok`, which succeeds fine without a replica set. + +If you've already started it, fix it in place — the entrypoint won't re-run once +the data directory has data: + +```bash +docker exec overleaf-mongo mongosh --quiet --eval \ + 'rs.initiate({_id:"overleaf",members:[{_id:0,host:"mongo:27017"}]})' +docker restart overleaf +``` + +**Use `docker exec`, not `docker compose`, for one-off commands.** OMV names the +env file `.env`, and Compose only auto-loads a file called `.env`. So a +manual compose command fails on the required variables even though the running +stack is perfectly configured: + +``` +required variable OVERLEAF_INVITE_TOKEN_SECRET is missing a value +``` + +The container already has the environment, so talk to it directly: + +```bash +docker exec overleaf bash -c \ + 'cd /overleaf/services/web && node modules/server-ce-scripts/scripts/create-user.mjs --admin --email=you@example.com' +``` + +If you'd rather have compose work by hand too, symlink it once — OMV keeps +passing its own `--env-file` and won't notice: + +```bash +ln -s overleaf.env .env +``` + +## Gotchas + +- **`OVERLEAF_SECURE_COOKIE` is tested with `!= null`.** Setting it to `"false"` + turns secure cookies **on**. The only way to disable it is to leave it unset — + and with it wrongly on over plain HTTP, login silently fails to stick. +- **Mongo 8.0 needs AVX.** Mongo 5.0+ won't start on CPUs without it, and the + failure is an immediate exit with nothing useful logged. Check first: + `grep -o avx2 /proc/cpuinfo | head -1` +- **A healthy Mongo does not mean the replica set is up.** The healthcheck runs + `db.stats().ok`, which passes without one — so `depends_on: service_healthy` + is satisfied and the app starts anyway, then fails its own DB check. +- **The replica-set script only runs on an empty data directory.** If Mongo has + data but no replica set, initialize by hand: + `docker compose exec mongo mongosh --eval 'rs.initiate({_id:"overleaf",members:[{_id:0,host:"mongo:27017"}]})'` +- **No signup page until the first user exists.** Use the `create-user.mjs` + script above; the web UI won't offer to make one. +- **Back up `DATA_PATH`.** `data/mongo` holds every project. `data/overleaf` + holds uploaded files and compile output. +- **Upgrading across majors** may need migrations, and Mongo itself can't jump + several versions at once. Read the release notes rather than pulling blind. + +## Health checks + +The image ships its own: + +```bash +docker compose exec sharelatex bash -c 'cd /overleaf/services/web && node modules/server-ce-scripts/scripts/check-mongodb.mjs' +docker compose exec sharelatex bash -c 'cd /overleaf/services/web && node modules/server-ce-scripts/scripts/check-redis.mjs' +docker compose exec mongo mongosh --quiet --eval 'rs.status().members.map(m => m.name + " " + m.stateStr)' +``` + +## Links + +- Upstream repo: +- Their sample compose: +- The Toolkit (upstream's recommended installer): +- Image: diff --git a/overleaf/compose.yaml b/overleaf/compose.yaml new file mode 100644 index 0000000..06c4f45 --- /dev/null +++ b/overleaf/compose.yaml @@ -0,0 +1,78 @@ +name: overleaf + +services: + sharelatex: + image: sharelatex/sharelatex:6.2.2 + container_name: overleaf + restart: unless-stopped + depends_on: + mongo: + condition: service_healthy + redis: + condition: service_started + # Overleaf needs time to finish in-flight compiles on shutdown. + stop_grace_period: 60s + + ports: + - "${PORT:-8020}:80" + + volumes: + - ${DATA_PATH:-./data}/overleaf:/var/lib/overleaf + + environment: + OVERLEAF_APP_NAME: ${OVERLEAF_APP_NAME:-Overleaf} + + # These hostnames are compose SERVICE names, not container names. + OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex + # One setting, two variable names. Upstream's own comment: "unfortunately". + OVERLEAF_REDIS_HOST: redis + REDIS_HOST: redis + + # Required for invite tokens. openssl rand -base64 32 + OVERLEAF_INVITE_TOKEN_SECRET: ${OVERLEAF_INVITE_TOKEN_SECRET:?generate with openssl rand -base64 32} + + # Public address. Every generated link -- activation, password reset, + # project invite -- is built from this. Defaults to http://localhost, + # which is wrong the moment you use a port or a domain. + OVERLEAF_SITE_URL: ${OVERLEAF_SITE_URL:-http://localhost:8020} + + ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file" + ENABLE_CONVERSIONS: "true" + EMAIL_CONFIRMATION_DISABLED: "true" + + # Serving over HTTPS? Uncomment. The value is NOT parsed -- the code + # tests `!= null`, so even "false" switches it ON. Comment it out to + # disable, never set it to false. + #OVERLEAF_SECURE_COOKIE: "true" + + # DO NOT add SANDBOXED_COMPILES / DOCKER_RUNNER here. Upstream's sample + # ships them enabled, but they are Server Pro only and break compiles on + # Community Edition. See the README. + + mongo: + image: mongo:8.0 + container_name: overleaf-mongo + restart: unless-stopped + # Overleaf uses transactions, which require replica-set mode even on one node. + command: "--replSet overleaf" + volumes: + - ${DATA_PATH:-./data}/mongo:/data/db + - ./init/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js:ro + environment: + MONGO_INITDB_DATABASE: sharelatex + # rs.initiate() registers the member as "mongo:27017", so during init the + # container has to resolve that name to itself. + extra_hosts: + - mongo:127.0.0.1 + healthcheck: + test: echo 'db.stats().ok' | mongosh localhost:27017/test --quiet + interval: 10s + timeout: 10s + retries: 5 + + redis: + image: redis:6.2 + container_name: overleaf-redis + restart: unless-stopped + volumes: + - ${DATA_PATH:-./data}/redis:/data diff --git a/overleaf/docker-compose.yaml b/overleaf/docker-compose.yaml deleted file mode 100644 index b0921e1..0000000 --- a/overleaf/docker-compose.yaml +++ /dev/null @@ -1,60 +0,0 @@ -volumes: - web: - mongo: - redis: - mongo-init-replica: - -services: - web: - image: sharelatex/sharelatex:4.2.3@sha256:6c6498de22775f1227eed7cf472c764aa851f0fe5b9e640d989ea8a90837159e - depends_on: - mongo: - condition: service_healthy - redis: - condition: service_started - links: - - mongo - - redis - ports: - - 8020:80 - volumes: - - ./data/sharelatex:/var/lib/sharelatex - environment: - SHARELATEX_APP_NAME: Overleaf - SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex - SHARELATEX_REDIS_HOST: redis - REDIS_HOST: redis - ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file" - ENABLE_CONVERSIONS: "true" - EMAIL_CONFIRMATION_DISABLED: "true" - TEXMFVAR: /var/lib/sharelatex/tmp/texmf-var - - mongo: - image: mongo:4.4@sha256:52c42cbab240b3c5b1748582cc13ef46d521ddacae002bbbda645cebed270ec0 - command: "--replSet overleaf" - expose: - - 27017 - volumes: - - ./data/mongo:/data/db - healthcheck: - test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet - interval: 10s - timeout: 10s - retries: 5 - - redis: - image: redis:6.2@sha256:5610a8ffeaf386ecc7b6c8f070662a7a9d01bc7eafe237152a7d16e05b84afc9 - expose: - - 6379 - volumes: - - ./data/redis:/data - - mongo-init-replica: - image: mongo:4.4 - depends_on: - - mongo - entrypoint: > - bash -c " - until mongo --host mongo --eval 'print(\"waiting for mongo\")'; do sleep 2; done; - mongo --host mongo --eval 'rs.initiate({_id: \"overleaf\", members: [{_id: 0, host: \"mongo:27017\"}]})'" - restart: "on-failure" \ No newline at end of file diff --git a/overleaf/init/mongodb-init-replica-set.js b/overleaf/init/mongodb-init-replica-set.js new file mode 100644 index 0000000..189dc5a --- /dev/null +++ b/overleaf/init/mongodb-init-replica-set.js @@ -0,0 +1,8 @@ +// Mounted into Mongo's /docker-entrypoint-initdb.d/ and run ONCE, on the very +// first start against an empty data directory. Overleaf needs transactions, +// which Mongo only offers in replica-set mode -- even for a single node. +// +// If you ever wipe ./data/mongo, this runs again. If Mongo has data but no +// replica set, it does NOT run and you must call rs.initiate() by hand. + +rs.initiate({ _id: 'overleaf', members: [{ _id: 0, host: 'mongo:27017' }] })