Skip to content

LabOS

Pre-release
Pre-release

Choose a tag to compare

@wildshark wildshark released this 29 Jul 11:20

LabOS — Student Coding Environment

A single docker compose project that gives every student a browser-based
VS Code editor, a full LAMP stack, a reverse proxy with SSL support, a
standalone web terminal, and nine language runtimes — all running from
one integrated terminal, with no Dockerfile required anywhere in the
project.


What's included

Service Purpose URL
vscode Browser-based VS Code editor http://localhost:8080
apache PHP web server http://localhost:8081
mysql MySQL 8 database internal only
phpmyadmin Database admin UI http://localhost:8082
nginx-proxy-manager Reverse proxy + free SSL http://localhost:81
ttyd Standalone web terminal http://localhost:7681
python-runner Python 3.12 terminal only
node-runner Node.js 20 terminal only
cpp-runner GCC / g++ terminal only
java-runner OpenJDK 21 terminal only
go-runner Go 1.22 terminal only
rust-runner Rust (stable) terminal only
ruby-runner Ruby 3.3 terminal only
dotnet-runner .NET SDK 8 terminal only

Architecture in one paragraph

The vscode container doesn't have language toolchains installed in it
directly. Instead, each language lives in its own lightweight sibling
container that just idles (sleep infinity), sharing the ./workspace
folder. vscode is given access to the host's Docker socket and a folder
of wrapper scripts (bin-wrappers/) placed first on its PATH. When a
student types python3 app.py, they're really running a wrapper that does
docker exec into the Python container — it feels local, but stays cleanly
isolated per language.

⚠️ Security note: Docker socket access gives vscode root-equivalent
control over the host machine. This is a deliberate convenience tradeoff for
one trusted user on their own machine. Do not expose this setup to
multiple untrusted students or a shared/public server without redesigning
that part first.


Step-by-step setup

1. Prerequisites

docker --version
docker compose version   # must be v2+

Make sure these ports are free on your machine: 80, 443, 81, 7681, 8080, 8081, 8082.

2. Get the project

unzip student-coding-environment.zip -d labos
cd labos

3. Configure secrets

cp .env.example .env
nano .env

Fill in at minimum:

CODE_SERVER_PASSWORD=...
MYSQL_ROOT_PASSWORD=...
MYSQL_DATABASE=...
MYSQL_USER=...
MYSQL_PASSWORD=...

Never commit .env — it's already in .gitignore.

4. Start everything

docker compose up -d

First run pulls around 10 images (code-server, PHP, MySQL, phpMyAdmin, NPM,
ttyd, plus 8 language runtimes). Depending on your connection this can take
several minutes and several GB of disk space.

5. Confirm it's healthy

docker compose ps                 # everything should say "running" / "healthy"
docker compose logs -f vscode     # Ctrl+C once it settles

6. Open the services

Service URL Login
Editor http://localhost:8080 password = CODE_SERVER_PASSWORD
PHP app http://localhost:8081
phpMyAdmin http://localhost:8082 MYSQL_USER / MYSQL_PASSWORD
NPM admin http://localhost:81 see step 7
Web terminal http://localhost:7681 none by default — see step 7

7. First-login hardening

Nginx Proxy Manager: log in with admin@example.com / changeme,
then change both immediately — this default is public knowledge.

ttyd: ships with no authentication at all. Fine for local-only use.
If anyone else can reach this machine, edit the ttyd service's command:
in compose.yml to add basic auth before leaving it running:

command: ["ttyd", "-c", "student:changeme", "-p", "7681", "bash"]

8. Test the language runtimes

In the code-server terminal (:8080 → Terminal → New Terminal):

python3 -c "print('python ok')"
node -e "console.log('node ok')"
g++ main.cpp -o main && cpp-exec ./main
java -version
go version
cargo --version
ruby -v
dotnet --version

First call to any language may pause a couple seconds while its container
starts — that's normal.

9. Everyday commands

docker compose stop                 # pause, keep data
docker compose start                # resume
docker compose logs -f <service>    # debug one service
docker compose down                 # stop + remove containers, data persists

10. Full reset (destroys data)

docker compose down -v
rm -rf mysql_data code-server-data npm workspace/*

Using the languages

# Python
python3 app.py
pip install requests

# Node.js
node index.js
npm install express

# C++ (compile normally, run via cpp-exec — see note below)
g++ main.cpp -o main
cpp-exec ./main

# Java
javac Main.java
java Main

# Go
go build -o app .
go-exec ./app

# Rust
cargo run

# Ruby
ruby script.rb

# C# / .NET
dotnet run

Why cpp-exec / go-exec exist

Compiling happens inside that language's own container, so the resulting
binary is linked against that container's libraries — not vscode's.
Running ./main directly from the code-server shell can fail with a library
mismatch. cpp-exec / go-exec run the binary back inside the container it
was built in, so it always works. Rust, Java, Ruby, Python, and Node don't
have this problem — their wrappers already run everything in the right place.


Project layout

.
├── compose.yml
├── .env.example
├── .gitignore
├── httpd.conf
├── README.md
├── bin-wrappers/
│   ├── python3, python, pip, pip3
│   ├── node, npm, npx
│   ├── g++, gcc, make, cmake, gdb, cpp-exec
│   ├── java, javac, jar
│   ├── go, go-exec
│   ├── cargo, rustc, rust-exec
│   ├── ruby, gem, bundle, irb
│   └── dotnet
├── workspace/            # student code (shared by all containers)
├── mysql_data/            # MySQL data (gitignored, created at runtime)
├── code-server-data/       # code-server settings/extensions (gitignored)
└── npm/                     # Nginx Proxy Manager data + certs (gitignored)

Common tasks

Add a PHP extension: edit the apache service's command: block,
extend the docker-php-ext-install line.

Add another language: copy one of the *-runner service blocks, point it
at the right official image, add it to vscode's depends_on, and drop a
matching wrapper script into bin-wrappers/.


Known limitations

  • Fixed container names (student-env-python-runner, etc.) mean this is
    designed for one instance per host. Running it multiple times side by
    side (e.g. one per student on a shared server) will collide — that needs a
    different architecture with per-student project names and no shared Docker
    socket access.
  • Image size: with 9 language runtimes plus the rest of the stack, the
    first pull is several gigabytes.
  • No build caching: since there's no Dockerfile, the apache and
    vscode containers re-run their install commands on every restart, adding
    some startup time.