diff --git a/README.md b/README.md index 2964371..0492eb5 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ handled separately — see [Exposing services](#exposing-services). |---------|------|-------| | [audiobookshelf](audiobookshelf/) | 13378 | Audiobooks and podcasts, with per-user progress sync | | [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 | | [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 | diff --git a/codeserver/.env.example b/codeserver/.env.example new file mode 100644 index 0000000..62f319e --- /dev/null +++ b/codeserver/.env.example @@ -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 diff --git a/codeserver/README.md b/codeserver/README.md new file mode 100644 index 0000000..2116168 --- /dev/null +++ b/codeserver/README.md @@ -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://: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: +- Upstream: diff --git a/codeserver/compose.yaml b/codeserver/compose.yaml new file mode 100644 index 0000000..7ad405c --- /dev/null +++ b/codeserver/compose.yaml @@ -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"