When I started experimenting with LinuxServer.io’s OpenVSCode Server, I wanted a lightweight, browser-based IDE that I could run on my homelab server. My goals were simple:
- Keep my code in a persistent workspace.
- Add Python support without breaking auto-updates (I use Watchtower).
- Optionally mount remote code with SSHFS.
- Avoid annoying file-save permission errors (EACCES).
After some trial and error, I arrived at a setup that ticks all these boxes. Here’s how I built it.
Folder layout (on my host)
/home/<user>/docker/openvscode/
├── config/ # container config (LinuxServer standard)
├── workspace/ # can be ignored if you use top-level workspace/
├── .env
├── docker-compose.yaml
├── init/ # holds custom init scripts (10-python.sh, 20-extensions.sh)
│ ├── 10-python.sh
│ └── 20-extensions.sh
└── workspace/ # my main development folder (mapped to /workspace)
└── project/
BashAdding Python support to LinuxServer OpenVSCode Server
Image: lscr.io/linuxserver/openvscode-server
(does not ship with Python preinstalled).
The base OpenVSCode image doesn’t ship with Python. To keep Watchtower updates safe, I used LinuxServer’s custom-cont-init.d mechanism. Any script you place in init/
runs at startup as root, before the main app launches.
Create the init folder on the host:
mkdir -p /home/<user>/docker/open-VSCode-Server/init
Bash10-python.sh installs Python and standard dev tools:
#!/usr/bin/env sh
set -e
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv python3-dev build-essential \
shellcheck shfmt
apt-get clean
rm -rf /var/lib/apt/lists/*
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache \
python3 py3-pip python3-dev build-base \
shellcheck shfmt
else
echo "Unsupported base image: no apt-get or apk found" >&2
exit 1
fi
BashMake it executable:
chmod +x /home/<user>/docker/open-VSCode-Server/init/10-python.sh
Bash20-extensions.sh preinstalls useful VS Code extensions:
#!/usr/bin/env sh
set -e
# Python extensions
/app/openvscode-server/bin/openvscode-server --install-extension ms-python.python
#/app/openvscode-server/bin/openvscode-server --install-extension ms-toolsai.jupyter
# Bash extensions
/app/openvscode-server/bin/openvscode-server --install-extension mads-hartmann.bash-ide-vscode
/app/openvscode-server/bin/openvscode-server --install-extension timonwong.shellcheck
/app/openvscode-server/bin/openvscode-server --install-extension foxundermoon.shell-format
BashMake it executable:
chmod +x /home/<user>/docker/open-VSCode-Server/init/20-extensions.sh
BashThis way, every time the container restarts, Python and my preferred extensions are ready to go.
Docker Compose Setup
Here’s the base service definition:
services:
openvscode:
image: lscr.io/linuxserver/openvscode-server:latest
container_name: openvscode
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
- OPENAI_API_KEY=${OPENAI_API_KEY} # optional
volumes:
- /home/<user>/docker/openvscode/config:/config
- /home/<user>/docker/openvscode/workspace:/workspace
- /srv/sshfs/project:/workspace/project # optional SSHFS
- /home/<user>/docker/openvscode/init:/custom-cont-init.d
networks:
- proxy
labels:
- "traefik.enable=true"
# HTTP -> HTTPS redirect
- "traefik.http.routers.code.entrypoints=http"
- "traefik.http.routers.code.rule=Host(`vscode.example.com`)"
- "traefik.http.routers.code.middlewares=code-https-redirect@file,code-headers@file"
# HTTPS with CrowdSec + Auth
- "traefik.http.routers.code-secure.entrypoints=https"
- "traefik.http.routers.code-secure.rule=Host(`vscode.example.com`)"
- "traefik.http.routers.code-secure.tls=true"
- "traefik.http.routers.code-secure.service=code"
- "traefik.http.services.code.loadbalancer.server.port=3000"
- "traefik.docker.network=proxy"
- "traefik.http.routers.code-secure.middlewares=code-auth@file,crowdsec-bouncer@file,code-headers@file"
restart: unless-stopped
networks:
proxy:
external: true
YAMLThree workspace options
- Set up A (local only): use
workspace/
as your main dev folder. - Setup B (SSHFS inside workspace): mount a remote path into
workspace/python_practice
using a systemd unit + SSHFS. - Set up C (SSHFS elsewhere): mount remote paths under
/srv/sshfs/…
and map them selectively into/workspace
in the container.
This flexibility lets you decide whether all your projects live locally, remotely, or a mix of both.
Set up A – No SSHFS, just a local workspace (simple)
- Use this if
workspace/
It’s a standard local folder. - You can create/edit anything under
/workspace
in the container.
services:
openvscode:
image: lscr.io/linuxserver/openvscode-server:latest
container_name: openvscode
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
volumes:
- /home/<user>/docker/open-VSCode-Server/config:/config
- /home/<user>/docker/open-VSCode-Server/workspace:/workspace
- /home/<user>/docker/open-VSCode-Server/init:/custom-cont-init.d
networks: [proxy]
restart: unless-stopped
YAMLHost permissions (only for Setup A):
sudo chown -R 1000:1000 /home/<user>/docker/open-VSCode-Server/workspace
BashSet up B – SSHFS mounted inside my workspace/
(preferred if I want /workspace/python_practice
)
Use a systemd service to mount the remote folder directly onto workspace/python_practice
on the host.
Then a single bind to /workspace
In the container, there is enough.
systemd unit (host): /etc/systemd/system/sshfs-python_practice.service
[Unit]
Description=SSHFS mount for project
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/sshfs -f user@10.x.x.x:/home/user/project /home/<user>/docker/openvscode/workspace/project \
-o IdentityFile=/home/<user>/.ssh/id_ed25519 \
-o UserKnownHostsFile=/home/<user>/.ssh/known_hosts \
-o StrictHostKeyChecking=yes \
-o allow_other,uid=1000,gid=1000,umask=022 \
-o reconnect,ServerAliveInterval=30,ServerAliveCountMax=3
ExecStop=/bin/fusermount3 -u -z /home/<user>/docker/openvscode/workspace/project
Restart=on-failure
RestartSec=10
KillMode=process
[Install]
WantedBy=multi-user.target
BashEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now sshfs-python_practice.service
BashCompose volumes:
volumes:
- /home/<user>/docker/open-VSCode-Server/config:/config
- /home/<user>/docker/open-VSCode-Server/workspace:/workspace # includes /workspace/python_practice
- /home/<user>/docker/open-VSCode-Server/init:/custom-cont-init.d
BashSet up C (alternative), my SSHFS mounted elsewhere on the host (e.g., /srv/sshfs/python_practice)
If you prefer not to mount directly inside your workspace/
You can mount the remote folder to a separate host path like /srv/sshfs/python_practice
using a systemd unit. Then overlay that path into the container at /workspace/python_practice
.
systemd unit (host): /etc/systemd/system/sshfs-python_practice.service
[Unit]
Description=SSHFS mount for python_practice
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/sshfs -f <user>@10.x.x.x:/home/<user>/python/practice /srv/sshfs/python_practice \
-o IdentityFile=/home/<user>/.ssh/id_ed25519 \
-o UserKnownHostsFile=/home/<user>/.ssh/known_hosts \
-o StrictHostKeyChecking=yes \
-o allow_other,uid=1000,gid=1000,umask=022 \
-o reconnect,ServerAliveInterval=30,ServerAliveCountMax=3
ExecStop=/bin/fusermount3 -u -z /srv/sshfs/python_practice
Restart=on-failure
RestartSec=10
KillMode=process
[Install]
WantedBy=multi-user.target
BashEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now sshfs-python_practice.service
BashCompose volumes:
volumes:
- /home/<user>/docker/open-VSCode-Server/workspace:/workspace
- /srv/sshfs/python_practice:/workspace/python_practice # overrides just this subpath
- /home/<user>/docker/open-VSCode-Server/init:/custom-cont-init.d
BashFixing Permissions
If you see EACCES
Errors when saving:
- Make sure you’re saving inside
/workspace/...
(the bind-mounted folder). - Ensure the host workspace is owned by the container user:
sudo chown -R 1000:1000 workspace
BashFor SSHFS mounts, add options uid=1000,gid=1000,umask=022
so the container user has proper write access.
Conclusion
With this setup, I now have a self-hosted, browser-based IDE that:
- Updates automatically without losing my customisations.
- Provides full Python support with preinstalled extensions.
- Works seamlessly with both local and remote (SSHFS) code.
It’s lightweight, reliable, and a great way to turn any server into a full-featured dev environment accessible from anywhere.