Files
composes/code-server/README.md
T
tiennm99 a63c121075 feat(code-server): give the workspace its own volume
The workspace moves off the config volume onto code-server-workspace, so
wiping editor state and wiping code are separate acts.

The image only ever chowns the literal path /config/workspace, and reads
DEFAULT_WORKSPACE to pick the folder to open, so a named volume on /workspace
would come up root-owned and unwritable. Creating the directory in a local
Dockerfile seeds the volume with the right ownership instead.
2026-09-18 17:38:07 +07:00

93 lines
4.5 KiB
Markdown

# code-server
[VS Code in the browser](https://github.com/linuxserver/docker-code-server),
from the LinuxServer image, set up as a full remote dev box.
Comes with Go, Node.js 24, Python 3, and zsh via LinuxServer mods, plus `gh`,
`git`, `glab`, `unzip` and `zip` through `INSTALL_PACKAGES`. Git
author/committer identity is injected from `.env`.
## Docker access
The `universal-docker` mod installs the Docker CLI but no daemon, so the host
socket is bind-mounted at `/var/run/docker.sock` to give it something to talk
to. Containers started from inside are siblings on the host, not children --
bind mounts in them resolve against host paths, so a path under `/config` will
not exist unless the same path exists on the host.
The socket is owned by the host's `docker` group, which the `abc` user inside
the container is not a member of; run `docker` under `sudo` (the `SUDO_PASSWORD`
is the same `PASSWORD`) or add the group by hand. Handing a container the
socket is equivalent to giving it root on the host -- that is accepted here
because this is a single-user dev box.
The mount carries `:ro`, which is not a security boundary: it only marks the
socket file read-only, while the Docker API is reached by connecting to the
socket, which a read-only mount does not stop. Full API access, and with it
root on the host, remains. Restricting that would need a socket proxy or a
separate rootless daemon.
## Environment
| Variable | Purpose |
| --- | --- |
| `SERVICE_HOSTNAME` | Container hostname, and the name the shell prompt shows. |
| `PASSWORD` | Web UI login, also the in-container sudo password. **A blank value disables authentication entirely.** |
| `GIT_NAME` / `GIT_EMAIL` | Git author and committer identity |
Generate a password with `openssl rand -base64 24`.
`SERVICE_HOSTNAME` is used twice: as the container's `hostname:` and as the
`HOST` variable inside it. Coolify injects `HOST=0.0.0.0` into every compose
app, and zsh seeds `$HOST` and the `%m`/`%M` prompt escapes from that variable
rather than calling `gethostname()` -- so the prompt reads `0`, the first
dot-separated field of `0.0.0.0`. code-server itself never reads `HOST` -- it
binds `[::]:8443` -- so overriding it only affects the prompt. bash is
unaffected; its `\h` uses the real hostname.
It is not called `HOSTNAME`, the obvious name, because Compose interpolation
lets the deploying shell's environment win over the `.env` file, and `HOSTNAME`
is set in every container -- including the one Coolify itself runs in. The
container would silently take Coolify's hostname instead of this value.
## Networking
Listens on `8443`. No ports are published — point the domain at that port in
Coolify or Dokploy. See the [root README](../README.md) for why.
## Storage
| Volume | Mount | Holds |
| --- | --- | --- |
| `code-server-config` | `/config` | Home directory: settings, extensions, shell history, CLI logins |
| `code-server-workspace` | `/workspace` | Code you work on |
Two volumes, the same split [paseo](../paseo/README.md) and
[opencode-web](../opencode-web/README.md) use: home in one, the workspace in
the other. Code survives a wipe of the editor's state, and the editor's state
survives a wipe of the code.
`DEFAULT_WORKSPACE` points at `/workspace` to match. It only chooses the folder
code-server opens; it does not move anything.
The `Dockerfile` exists only because of that move. The image hard-codes what it
hands to the `abc` user — `init-adduser` takes `/app`, `/config` and
`/defaults`, `init-code-server` takes `/config/workspace` by literal path — and
reads `DEFAULT_WORKSPACE` only to decide which folder to open. A named volume
on `/workspace` is therefore never chowned, comes up `root:root`, and the
editor cannot write a single file into it.
Creating the directory in the image fixes it without any runtime step: Docker
seeds an empty named volume from the image directory, ownership included, so
`/workspace` arrives owned by `abc`. It is the same reason `paseo` needs no
fixup — its upstream image ships `/workspace` already owned.
The alternative was a `chown` script in `/custom-cont-init.d`, the image's own
init hook. It was rejected because it needs a bind mount from the repository
into the container, and because the hook silently skips any script that has
lost its executable bit — a read-only workspace with nothing obvious to blame.
Baking `1000:1000` into the image costs the ability to change `PUID` at
runtime, which is free here: both services pin it to `1000`.
Anything outside these two volumes is lost on redeploy.