12 KiB
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
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 — see below, there is no signup form at all.
Adding users
Community Edition has no public registration. /register exists but renders
a page pointing at an admin rather than a form, and there's no setting to open
it up. Accounts are created one at a time by you.
# the first account -- make it an admin
docker compose exec sharelatex bash -c \
'cd /overleaf/services/web && node modules/server-ce-scripts/scripts/create-user.mjs --admin --email=you@example.com'
# everyone after that -- drop --admin
docker compose exec sharelatex bash -c \
'cd /overleaf/services/web && node modules/server-ce-scripts/scripts/create-user.mjs --email=someone@example.com'
The command prints an activation URL:
http://overleaf.example.com/user/activate?token=<token>&user_id=<id>
Send that link to the person yourself. No mail is configured here, so
Overleaf's attempt to email it goes nowhere. They open it, choose their own
password, and they're in — there's no confirmation step, since
EMAIL_CONFIRMATION_DISABLED is set.
The host in that URL comes from OVERLEAF_SITE_URL. Get that wrong and the
link points somewhere unreachable — it defaults to bare http://localhost.
Once an admin exists you can do the same thing from the web UI at Admin → Manage Users, which shows the activation link on screen. Same result; the CLI is just faster over SSH.
On OMV, use docker exec instead of docker compose exec — see
Running it on OpenMediaVault.
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
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:
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:
# .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):
overleaf.example.com {
reverse_proxy 192.168.1.50:8020
}
That's the whole config — Caddy gets the certificate itself. See reverse proxy.
A 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:
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 <stack>.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:
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:
ln -s overleaf.env .env
Gotchas
OVERLEAF_SECURE_COOKIEis 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 start_periodon Mongo's healthcheck is load-bearing. Without it, slow checks during startup count againstretriesstraight away — five of them (~50s) mark Mongo unhealthy, and the app'scondition: service_healthyrefuses to start it. Mongo then recovers on its next check, so by morning everything looks fine except the app, which is simply down. This bites on backup scripts that stop and start containers: Mongo comes back while the disks are still busy,mongoshis a Node process and answers slowly, and the stack loses a race nobody knew it was running. Upstream's sample has nostart_periodeither.- A healthy Mongo does not mean the replica set is up. The healthcheck runs
db.stats().ok, which passes without one — sodepends_on: service_healthyis 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.mjsscript above; the web UI won't offer to make one. - Missing LaTeX packages — see the section below. The short version is
tlmgr --usermode install <pkg>, not plaintlmgr install. - Back up
DATA_PATH.data/mongoholds every project.data/overleafholds 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.
Installing LaTeX packages so they stay installed
The image ships a reduced TeX Live, so a document can fail on a package that
exists upstream — booktabs is a common first casualty. The obvious fix works
and then quietly undoes itself:
docker exec overleaf tlmgr install booktabs # works now, gone after an upgrade
TeX Live lives at /usr/local/texlive/2026/, inside the image. A
docker restart keeps those files; recreating the container after
docker compose pull does not. A document that compiled last month stops
compiling and nothing tells you why.
The fix is user-mode installs plus a TEXMFHOME that both users agree on.
That second half matters more than it looks: tlmgr runs as root, but the
compiles run as www-data, and by default those two resolve TEXMFHOME
differently:
root /root/texmf <- where a plain `tlmgr --usermode install` lands
www-data /var/www/texmf <- where LaTeX actually looks
So the install succeeds, reports no error, and the package still isn't found —
and /root isn't even readable by www-data, so no amount of mounting that
path helps. This stack therefore sets:
TEXMFHOME: /var/lib/overleaf/texmf
which is inside the persisted volume, so both users agree and the packages survive image updates. Then:
docker exec overleaf tlmgr --usermode init-usertree # once
docker exec overleaf tlmgr --usermode install booktabs
Verify as the user that actually compiles, not as root:
docker exec -u www-data overleaf kpsewhich booktabs.sty
# /var/lib/overleaf/texmf/... correct, persisted
# /usr/local/texlive/... inside the image, lost on update
# nothing installed somewhere the compiler can't see
Caveats. User mode handles ordinary style packages cleanly. Packages that
install fonts or need map-file updates may also want updmap-user, and a few
that expect to modify the main tree won't work this way — install those with a
custom image instead:
FROM sharelatex/sharelatex:6.2.2
RUN tlmgr install <packages>
That's the more reproducible option in general, since the package list ends up in version control. The trade is rebuilding on every upstream release. If you stay with user mode, keep a list of what you've installed — the volume survives, but a list is what lets you rebuild from nothing.
Health checks
The image ships its own:
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: https://github.com/overleaf/overleaf
- Their sample compose: https://github.com/overleaf/overleaf/blob/main/docker-compose.yml
- The Toolkit (upstream's recommended installer): https://github.com/overleaf/toolkit
- Image: https://hub.docker.com/r/sharelatex/sharelatex