Building a Self-Hosted OpenVSCode Dev Environment with Docker

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/
Bash

Adding 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
Bash

10-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
Bash

Make it executable:

chmod +x /home/<user>/docker/open-VSCode-Server/init/10-python.sh
Bash

20-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
Bash

Make it executable:

chmod +x /home/<user>/docker/open-VSCode-Server/init/20-extensions.sh
Bash

This 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

YAML

Three 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
YAML

Host permissions (only for Setup A):

sudo chown -R 1000:1000 /home/<user>/docker/open-VSCode-Server/workspace
Bash

Set 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
Bash

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now sshfs-python_practice.service
Bash

Compose 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
Bash

Set 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
Bash

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now sshfs-python_practice.service
Bash

Compose 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
Bash

Fixing 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
Bash

For 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.