Compare commits

..

2 Commits

Author SHA1 Message Date
Goyban 1ad05be5b6 added code-server 2026-08-25 14:04:41 +02:00
Goyban fa891a6318 overleaf: keep tlmgr packages across image updates 2026-08-25 14:03:48 +02:00
6 changed files with 271 additions and 0 deletions
+1
View File
@@ -38,6 +38,7 @@ handled separately — see [Exposing services](#exposing-services).
|---------|------|-------| |---------|------|-------|
| [audiobookshelf](audiobookshelf/) | 13378 | Audiobooks and podcasts, with per-user progress sync | | [audiobookshelf](audiobookshelf/) | 13378 | Audiobooks and podcasts, with per-user progress sync |
| [cloudflare-tunnel](cloudflare-tunnel/) | — | Outbound connector. Exposes other services without opening a port | | [cloudflare-tunnel](cloudflare-tunnel/) | — | Outbound connector. Exposes other services without opening a port |
| [codeserver](codeserver/) | 8443 | VS Code in a browser. Effectively a remote shell — read its README before exposing |
| [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery | | [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 | | [openwebui](openwebui/) | 3033 | LLM chat UI. One `OWUI_TAG` switches between CPU, CUDA and bundled-Ollama |
| [overleaf](overleaf/) | 8020 | LaTeX editor. Four containers: app, Mongo (replica set), Redis, init | | [overleaf](overleaf/) | 8020 | LaTeX editor. Four containers: app, Mongo (replica set), Redis, init |
+57
View File
@@ -0,0 +1,57 @@
# Copy to .env and adjust. .env is git-ignored; this file is not.
# ─── Required ───────────────────────────────────────────────────────
# Web UI password. There is no default and no "first run" setup: if neither
# PASSWORD nor HASHED_PASSWORD is set, code-server serves an editor and a
# terminal with NO authentication whatsoever.
PASSWORD=
# Instead of the plaintext above, you can store only a hash. It overrides
# PASSWORD when set. Generate one with:
#
# echo -n "yourpassword" | npx argon2-cli -e
#
# Then uncomment HASHED_PASSWORD in compose.yaml.
#HASHED_PASSWORD=
# ─── Sudo inside the container ──────────────────────────────────────
# Set this if you want to install things from the built-in terminal. The image
# is deliberately minimal -- no python, no pip, no compilers -- so anything
# beyond editing needs apt:
#
# sudo apt update && sudo apt install -y python3-pip
#
# Leaving it empty is safe and simply disables sudo; the init script tests for
# a non-empty value, so empty is not an enabled blank password. It can be the
# same as PASSWORD or different -- it only guards `sudo` inside the container.
#
# Be aware of what it grants: root in the container. That is a container, not
# your host, but it is still root over anything you have mounted.
SUDO_PASSWORD=
# Store a hash instead of the plaintext. Takes priority over SUDO_PASSWORD.
# Format is $type$salt$hash. Uncomment the matching line in compose.yaml.
#SUDO_PASSWORD_HASH=
# ─── Identity ───────────────────────────────────────────────────────
# Files you create in the editor are owned by this user. `id -u` / `id -g`.
# Defaulted to 1000/1000 in compose.yaml.
#PUID=1000
#PGID=1000
#TZ=Europe/Paris
# ─── Paths ──────────────────────────────────────────────────────────
# Settings, installed extensions, and the default workspace.
#CONFIG_PATH=./config
# Optional: existing code on the host to expose in the editor. Uncomment the
# matching volume line in compose.yaml too.
#PROJECTS_PATH=/srv/projects
# ─── Ports and proxying ─────────────────────────────────────────────
#PORT=8443
#DEFAULT_WORKSPACE=/config/workspace
# Only for subdomain port-forwarding (3000.code.example.com). Needs wildcard
# DNS and a wildcard certificate; skip it if you don't need that.
#PROXY_DOMAIN=code.example.com
+112
View File
@@ -0,0 +1,112 @@
# code-server
VS Code in a browser. Useful for editing on the NAS from a laptop, a tablet, or
any machine you don't want to set a toolchain up on.
## Quick start
```bash
cp .env.example .env
$EDITOR .env # PASSWORD is required
docker compose up -d
```
Open `http://<host>:8443`. The password from `.env` is the only credential.
## Ports
| Port | Proto | Purpose |
|------|-------|---------|
| 8443 | http | Editor UI (plain HTTP — put TLS in front of it) |
## Read this before exposing it
code-server is not a document editor with a terminal bolted on. It **is** a
shell, with a file browser, running as `PUID`/`PGID`, with network access to
everything the container can reach. Treat exposing it exactly as you'd treat
exposing SSH.
Three consequences worth being deliberate about:
**`PASSWORD` is required here** (`${PASSWORD:?…}`), which is a deviation from
upstream's example. Their compose marks it `#optional`, and it is — but the
docs also say: *"If PASSWORD or HASHED_PASSWORD is not provided, there will be
no auth."* Optional-and-silently-open is the wrong default for something that
hands out a shell, so this stack refuses to start without one.
**`SUDO_PASSWORD` is how you install anything.** The image is deliberately
minimal — no python, no pip, no compilers — so `sudo apt install python3-pip`
is the normal first move, and that needs sudo enabled. It grants root *in the
container*, which is a real boundary but not your host. Leave it empty if you
only ever edit; the init script tests for a non-empty value, so empty means
disabled rather than an enabled blank password.
**Whatever you mount is executable, not just editable.** The commented
`PROJECTS_PATH` line is genuinely convenient and genuinely a decision — mount
your code, not your whole home directory. Never mount `/var/run/docker.sock`
here; that converts a container password into root on the host.
## Why it looks like this
**LinuxServer image**, so the usual `PUID`/`PGID`/`TZ` contract applies and
files you create are owned by you rather than root. Same reasoning as
[jellyfin](../jellyfin/) — one dialect across the stacks.
**`HASHED_PASSWORD` is offered but commented.** `.env` is git-ignored, so a
plaintext password there is not exposed, but the hash means the credential
isn't sitting in a readable file at all. Generate one with
`echo -n "yourpassword" | npx argon2-cli -e`. It overrides `PASSWORD` when set.
**`PROXY_DOMAIN` is commented out.** It only matters if you want forwarded
ports served as subdomains — port 3000 as `3000.code.example.com` — which needs
a wildcard DNS record and a wildcard certificate. Without it, forwarded ports
still work through the editor's own proxy path.
**`DEFAULT_WORKSPACE` is set** so the editor opens somewhere useful instead of
an empty file picker on every visit.
## What survives a rebuild, and what doesn't
`/config` is the app user's home directory — `abc:x:911:1001::/config` — and
it's the only persisted path. That single fact decides how you should install
things:
| Installed with | Lands in | Survives `docker compose up` after a pull? |
|---|---|---|
| `sudo apt install …` | `/usr/…` | **No** — gone with the old container |
| `pip install --user …` | `/config/.local` | Yes |
| VS Code extensions | `/config` | Yes |
| Anything in your workspace | `/config/workspace` | Yes |
So `sudo apt install python3-pip` has to be repeated after every image update,
while the packages you then `pip install --user` stay put. Prefer `--user`
installs, and keep a note of the apt packages you need so you can replay them.
If a toolchain matters enough that re-running apt annoys you, build a small
image `FROM lscr.io/linuxserver/code-server` with the packages baked in.
## Gotchas
- **Extensions live in `/config`.** They're part of what to back up, and
they're why the config directory grows.
- **The VS Code Marketplace isn't available.** code-server uses Open VSX, so a
few Microsoft-licensed extensions are missing. Most have equivalents.
- **Port forwarding is per-session.** Ports you open inside the editor are
reachable through it, not published on the host.
- **A container password is a weaker boundary than it looks.** Anyone who logs
in can reach every service on your Docker network directly by name.
## Exposing it
- [Reverse proxy with Caddy](../docs/reverse-proxy.md) — the minimum. Never
serve this over plain HTTP off your LAN; the password crosses the wire.
- [Single sign-on with Authentik](../docs/authentik-sso.md) — a good fit here.
Forward auth in front of code-server means an attacker needs to get past
Authentik before reaching a login form that leads to a shell.
- [Cloudflare Tunnel](../cloudflare-tunnel/) — works, and avoids opening a port.
Pair it with Cloudflare Access rather than relying on the password alone.
## Links
- Image docs: <https://docs.linuxserver.io/images/docker-code-server/>
- Upstream: <https://github.com/coder/code-server>
+44
View File
@@ -0,0 +1,44 @@
services:
code-server:
image: lscr.io/linuxserver/code-server:latest
container_name: code-server
restart: unless-stopped
environment:
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
- TZ=${TZ:-Etc/UTC}
# REQUIRED. With neither PASSWORD nor HASHED_PASSWORD set, code-server
# starts with NO authentication -- an editor and a shell, open to anyone
# who can reach the port.
- PASSWORD=${PASSWORD:?set one - without it code-server has no auth at all}
# Overrides PASSWORD when set. Use it to keep the plaintext out of .env.
#- HASHED_PASSWORD=${HASHED_PASSWORD}
# Enables `sudo` in the built-in terminal -- needed to apt-install
# anything, since the image ships no python/pip/compilers. Empty is safe
# and simply leaves sudo disabled: the init script tests `-n`, so an
# empty value is not the same as an enabled blank password.
- SUDO_PASSWORD=${SUDO_PASSWORD:-}
# Takes priority over SUDO_PASSWORD if you'd rather store only a hash.
#- SUDO_PASSWORD_HASH=${SUDO_PASSWORD_HASH}
# Only needed if you serve forwarded ports on subdomains, so that
# port 3000 becomes 3000.code.example.com. Requires a wildcard DNS
# record and a wildcard certificate.
#- PROXY_DOMAIN=${PROXY_DOMAIN}
- DEFAULT_WORKSPACE=${DEFAULT_WORKSPACE:-/config/workspace}
#- PWA_APPNAME=${PWA_APPNAME:-code-server}
volumes:
# Settings, extensions, and the default workspace. Back this up.
- ${CONFIG_PATH:-./config}:/config
# Code that lives elsewhere on the host. Whatever you mount here is
# editable -- and runnable -- from the browser.
#- ${PROJECTS_PATH}:/config/workspace/projects
ports:
- "${PORT:-8443}:8443"
+53
View File
@@ -167,11 +167,64 @@ ln -s overleaf.env .env
`docker compose exec mongo mongosh --eval 'rs.initiate({_id:"overleaf",members:[{_id:0,host:"mongo:27017"}]})'` `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` - **No signup page until the first user exists.** Use the `create-user.mjs`
script above; the web UI won't offer to make one. script 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 plain `tlmgr install`.
- **Back up `DATA_PATH`.** `data/mongo` holds every project. `data/overleaf` - **Back up `DATA_PATH`.** `data/mongo` holds every project. `data/overleaf`
holds uploaded files and compile output. holds uploaded files and compile output.
- **Upgrading across majors** may need migrations, and Mongo itself can't jump - **Upgrading across majors** may need migrations, and Mongo itself can't jump
several versions at once. Read the release notes rather than pulling blind. 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:
```bash
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 `TEXMFHOME`, which this stack bind-mounts to `${DATA_PATH}/texmf`.
Install in user mode and packages land on the volume instead:
```bash
# once, to create the tree
docker exec overleaf tlmgr init-usertree
# thereafter, for each package
docker exec overleaf tlmgr --usermode install booktabs
```
Verify it took:
```bash
docker exec overleaf kpsewhich booktabs.sty
# /root/texmf/tex/latex/booktabs/booktabs.sty <- persisted
# /usr/local/texlive/... <- inside the image, will be lost
```
The path in the output tells you which mode you used.
**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:
```dockerfile
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 ## Health checks
The image ships its own: The image ships its own:
+4
View File
@@ -18,6 +18,10 @@ services:
volumes: volumes:
- ${DATA_PATH:-./data}/overleaf:/var/lib/overleaf - ${DATA_PATH:-./data}/overleaf:/var/lib/overleaf
# TEXMFHOME. Packages installed with `tlmgr --usermode install` land here
# instead of inside the image, so they survive an image update. See the
# README -- without this, every upgrade silently drops them.
- ${DATA_PATH:-./data}/texmf:/root/texmf
environment: environment:
OVERLEAF_APP_NAME: ${OVERLEAF_APP_NAME:-Overleaf} OVERLEAF_APP_NAME: ${OVERLEAF_APP_NAME:-Overleaf}