2026-06-09 16:06:02 +02:00
|
|
|
# ppy: the single shared image every DevPlace container instance runs.
|
|
|
|
|
# Build once with `make ppy`. Context is devplacepy/services/containers/files.
|
|
|
|
|
FROM python:3.13-slim-bookworm AS base
|
|
|
|
|
|
|
|
|
|
ENV PYTHONUNBUFFERED=1 \
|
|
|
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
|
|
|
PIP_NO_CACHE_DIR=1 \
|
|
|
|
|
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
2026-06-14 16:46:36 +02:00
|
|
|
PIP_DEFAULT_TIMEOUT=120 \
|
|
|
|
|
PIP_RETRIES=10 \
|
2026-06-09 16:06:02 +02:00
|
|
|
DEBIAN_FRONTEND=noninteractive \
|
|
|
|
|
PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
|
|
|
|
|
|
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
|
|
git curl wget vim ack ca-certificates build-essential libpq-dev \
|
2026-06-11 14:06:17 +02:00
|
|
|
tmux apache2-utils procps htop iftop iotop netcat-openbsd zip unzip \
|
2026-08-07 10:53:08 +02:00
|
|
|
fakeroot xz-utils pkg-config \
|
|
|
|
|
binutils gnupg2 libc6-dev libcurl4-openssl-dev libedit2 libedit-dev \
|
|
|
|
|
libncurses-dev libpython3-dev libsqlite3-0 libsqlite3-dev uuid-dev \
|
|
|
|
|
libxml2-dev libz3-dev tzdata zlib1g-dev \
|
2026-06-09 16:06:02 +02:00
|
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
|
|
2026-08-07 10:53:08 +02:00
|
|
|
ENV RUSTUP_HOME=/opt/rust/rustup \
|
|
|
|
|
CARGO_HOME=/opt/rust/cargo \
|
|
|
|
|
CHOOSENIM_HOME=/opt/nim/choosenim \
|
|
|
|
|
CHOOSENIM_DIR=/opt/nim/toolchains \
|
|
|
|
|
NIMBLE_DIR=/opt/nim/nimble \
|
|
|
|
|
SWIFTLY_HOME_DIR=/opt/swift/swiftly \
|
|
|
|
|
SWIFTLY_BIN_DIR=/opt/swift/bin \
|
|
|
|
|
SWIFT_HOME=/opt/swift/toolchain
|
|
|
|
|
|
|
|
|
|
RUN curl -fsSL https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal \
|
|
|
|
|
&& rm -rf "$CARGO_HOME/registry" "$CARGO_HOME/git" \
|
|
|
|
|
&& "$CARGO_HOME/bin/rustc" --version
|
|
|
|
|
|
|
|
|
|
RUN curl -fsSL https://nim-lang.org/choosenim/init.sh | sh -s -- -y || true; \
|
|
|
|
|
rm -rf "$CHOOSENIM_HOME/downloads"; \
|
|
|
|
|
"$NIMBLE_DIR/bin/nim" --version | head -1
|
|
|
|
|
|
|
|
|
|
RUN set -eu; \
|
|
|
|
|
curl -fsSL "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" -o /tmp/swiftly.tar.gz; \
|
|
|
|
|
mkdir -p /tmp/swiftly-unpack; \
|
|
|
|
|
tar -xzf /tmp/swiftly.tar.gz -C /tmp/swiftly-unpack; \
|
|
|
|
|
/tmp/swiftly-unpack/swiftly init --assume-yes --skip-install --no-modify-profile; \
|
|
|
|
|
"$SWIFTLY_BIN_DIR/swiftly" install latest --assume-yes; \
|
|
|
|
|
toolchain="$(find /root/.local/share/swiftly/toolchains -mindepth 1 -maxdepth 1 -type d | head -1)"; \
|
|
|
|
|
[ -n "$toolchain" ] || { echo "swift toolchain not found after install"; exit 1; }; \
|
|
|
|
|
mv "$toolchain" /opt/swift/toolchain; \
|
|
|
|
|
rm -rf /tmp/swiftly.tar.gz /tmp/swiftly-unpack /root/.local/share/swiftly \
|
|
|
|
|
"$SWIFTLY_HOME_DIR" "$SWIFTLY_BIN_DIR"; \
|
|
|
|
|
/opt/swift/toolchain/usr/bin/swift --version
|
|
|
|
|
|
2026-06-09 16:06:02 +02:00
|
|
|
FROM base AS deps
|
|
|
|
|
RUN pip install \
|
|
|
|
|
pip setuptools wheel packaging \
|
|
|
|
|
requests urllib3 certifi idna charset-normalizer httpx aiohttp \
|
|
|
|
|
pydantic typing-extensions python-dotenv aiofiles \
|
|
|
|
|
python-dateutil six pytz \
|
|
|
|
|
pyyaml toml \
|
|
|
|
|
loguru structlog tenacity rich tqdm click typer
|
|
|
|
|
RUN pip install \
|
|
|
|
|
numpy pandas matplotlib plotly \
|
|
|
|
|
sqlalchemy psycopg2-binary redis dataset
|
|
|
|
|
RUN pip install \
|
|
|
|
|
flask django fastapi starlette "uvicorn[standard]" gunicorn \
|
2026-06-10 09:21:05 +02:00
|
|
|
h2 celery apscheduler watchfiles \
|
2026-06-09 16:06:02 +02:00
|
|
|
websocket-client websockets \
|
|
|
|
|
beautifulsoup4 lxml scrapy
|
|
|
|
|
RUN pip install \
|
|
|
|
|
cryptography pycryptodome pillow \
|
|
|
|
|
pytest ruff black isort flake8 mypy \
|
|
|
|
|
sphinx mkdocs poetry pipenv hatch \
|
|
|
|
|
reflex
|
|
|
|
|
RUN pip install playwright && playwright install --with-deps chromium
|
|
|
|
|
|
|
|
|
|
FROM deps AS runtime
|
Make dev workspaces serve a working browser IDE end to end
The workspace feature shipped its routes, agent tools and docs, but the
editor was never reachable: the project page had no entry point, the ppy
image had no code-server binary, no certificate was ever requested for a
tunnel, and both nginx and the proxy dropped what the editor needs.
- Add a VS Code button to the project action row and a Workspace item to
the overflow menu, gated by can_open_workspace plus a running instance
(viewer_can_workspace and workspace_editor_url on ProjectDetailOut).
- Install a pinned code-server in ppy.Dockerfile before USER pravda and
assert it in the build smoke test, so an image that cannot run the
editor no longer builds green.
- Run the editor with --auth password and a per workspace 8 character
pronounceable secret, minted once at the ensure_editor_password choke
point and injected as PASSWORD. Keep it off WorkspaceViewOut, which the
admin listing shares.
- Publish the editor tunnel when a workspace is created and issue its
certificate from a new WorkspaceService phase against the molohttp admin
API, then notify the owner with the live URL and the password. Only
pending rows are retried, so a broken host cannot burn the ACME failure
rate limit. Renewal stays molohttp's job.
- Forward the original Host on proxied requests and the client cookie on
proxied websockets, so code-server scopes its session cookie to the
public hostname and authenticates the workbench socket.
- Recreate a container stuck in the created state instead of retrying
docker start forever against an image it can no longer run.
- Return a JSON string from WorkspaceController.dispatch; raw dicts landed
in a tool message and aborted the turn at the model endpoint.
- Let the nginx catch-all carry websocket upgrades, keeping upstream
keepalive, so tunnelled apps and the editor both connect.
2026-08-07 13:46:40 +02:00
|
|
|
ARG CODE_SERVER_VERSION=4.131.0
|
|
|
|
|
RUN set -eu; \
|
|
|
|
|
case "$(dpkg --print-architecture)" in \
|
|
|
|
|
amd64) arch=amd64 ;; \
|
|
|
|
|
arm64) arch=arm64 ;; \
|
|
|
|
|
*) echo "unsupported architecture: $(dpkg --print-architecture)"; exit 1 ;; \
|
|
|
|
|
esac; \
|
|
|
|
|
curl -fsSL -o /tmp/code-server.tar.gz \
|
|
|
|
|
"https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server-${CODE_SERVER_VERSION}-linux-${arch}.tar.gz"; \
|
|
|
|
|
mkdir -p /usr/local/lib/code-server; \
|
|
|
|
|
tar -xzf /tmp/code-server.tar.gz -C /usr/local/lib/code-server --strip-components=1; \
|
|
|
|
|
rm -f /tmp/code-server.tar.gz; \
|
|
|
|
|
ln -sf /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server
|
2026-08-10 00:23:20 +02:00
|
|
|
COPY vscode/devplace-workspace /usr/local/lib/code-server/lib/vscode/extensions/devplace-workspace
|
|
|
|
|
COPY vscode/branding/favicon.ico /usr/local/lib/code-server/src/browser/media/favicon.ico
|
|
|
|
|
COPY vscode/branding/favicon.svg /usr/local/lib/code-server/src/browser/media/favicon.svg
|
|
|
|
|
COPY vscode/branding/favicon-dark-support.svg /usr/local/lib/code-server/src/browser/media/favicon-dark-support.svg
|
|
|
|
|
COPY vscode/branding/pwa-icon-192.png /usr/local/lib/code-server/src/browser/media/pwa-icon-192.png
|
|
|
|
|
COPY vscode/branding/pwa-icon-512.png /usr/local/lib/code-server/src/browser/media/pwa-icon-512.png
|
|
|
|
|
COPY vscode/branding/pwa-icon-maskable-192.png /usr/local/lib/code-server/src/browser/media/pwa-icon-maskable-192.png
|
|
|
|
|
COPY vscode/branding/pwa-icon-maskable-512.png /usr/local/lib/code-server/src/browser/media/pwa-icon-maskable-512.png
|
|
|
|
|
COPY vscode/branding/devplace-login.css /tmp/devplace-login.css
|
|
|
|
|
COPY vscode/product.patch.json /tmp/product.patch.json
|
|
|
|
|
RUN set -eu; \
|
|
|
|
|
cat /tmp/devplace-login.css >> /usr/local/lib/code-server/src/browser/pages/login.css; \
|
|
|
|
|
python3 -c "import json,pathlib; p=pathlib.Path('/usr/local/lib/code-server/lib/vscode/product.json'); d=json.loads(p.read_text()); d.update(json.loads(pathlib.Path('/tmp/product.patch.json').read_text())); p.write_text(json.dumps(d, indent=2))"; \
|
|
|
|
|
rm -f /tmp/devplace-login.css /tmp/product.patch.json
|
2026-06-09 16:06:02 +02:00
|
|
|
COPY sudo /usr/local/bin/sudo
|
2026-06-11 14:06:17 +02:00
|
|
|
COPY aptroot /usr/local/bin/aptroot
|
2026-06-09 16:06:02 +02:00
|
|
|
COPY pagent /usr/bin/pagent.py
|
2026-06-14 16:46:36 +02:00
|
|
|
COPY bot.py /usr/bin/botje.py
|
|
|
|
|
COPY d.py /usr/bin/d.py
|
2026-06-16 20:38:33 +02:00
|
|
|
COPY dpc /usr/bin/dpc
|
2026-06-09 16:06:02 +02:00
|
|
|
COPY .vimrc /home/pravda/.vimrc
|
|
|
|
|
RUN set -eu; \
|
2026-06-16 20:38:33 +02:00
|
|
|
chmod 0755 /usr/local/bin/sudo /usr/local/bin/aptroot /usr/bin/pagent.py /usr/bin/botje.py /usr/bin/d.py /usr/bin/dpc; \
|
2026-06-09 16:06:02 +02:00
|
|
|
ln -sf /usr/local/bin/sudo /usr/bin/sudo || true; \
|
2026-06-11 14:06:17 +02:00
|
|
|
for t in apt apt-get dpkg; do ln -sf /usr/local/bin/aptroot "/usr/local/bin/$t"; done; \
|
2026-06-09 16:06:02 +02:00
|
|
|
if getent passwd 1000 >/dev/null 2>&1; then \
|
|
|
|
|
existing="$(getent passwd 1000 | cut -d: -f1)"; \
|
|
|
|
|
if [ "$existing" != "pravda" ]; then userdel "$existing" 2>/dev/null || deluser "$existing" 2>/dev/null || true; fi; \
|
|
|
|
|
fi; \
|
|
|
|
|
if ! getent group pravda >/dev/null 2>&1; then groupadd --gid 1000 pravda 2>/dev/null || addgroup -g 1000 pravda 2>/dev/null || true; fi; \
|
|
|
|
|
if ! id pravda >/dev/null 2>&1; then \
|
|
|
|
|
useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash pravda 2>/dev/null \
|
|
|
|
|
|| adduser -u 1000 -G pravda -D -s /bin/sh pravda 2>/dev/null || true; \
|
|
|
|
|
fi; \
|
|
|
|
|
mkdir -p /app /home/pravda/.local/bin; \
|
2026-06-11 14:06:17 +02:00
|
|
|
for d in /app /home/pravda /usr/local/lib /usr/local/bin /usr/local/share /usr/lib/python3 /opt \
|
|
|
|
|
/usr/lib /usr/bin /usr/sbin /usr/share /usr/include /etc /var/lib /var/cache /var/log /srv; do \
|
2026-06-09 16:06:02 +02:00
|
|
|
[ -e "$d" ] && chown -R pravda:pravda "$d" 2>/dev/null || true; \
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
USER pravda
|
2026-08-07 10:53:08 +02:00
|
|
|
ENV PATH=/home/pravda/.local/bin:/opt/rust/cargo/bin:/opt/nim/nimble/bin:/opt/swift/toolchain/usr/bin:$PATH \
|
|
|
|
|
DEVPLACE_TOOLCHAINS=python,rust,nim,swift
|
2026-06-09 16:06:02 +02:00
|
|
|
WORKDIR /app
|
2026-08-07 10:53:08 +02:00
|
|
|
|
|
|
|
|
RUN printf '%s\n' \
|
|
|
|
|
'export RUSTUP_HOME=/opt/rust/rustup' \
|
|
|
|
|
'export CARGO_HOME=/opt/rust/cargo' \
|
|
|
|
|
'export NIMBLE_DIR=/opt/nim/nimble' \
|
|
|
|
|
'export SWIFT_HOME=/opt/swift/toolchain' \
|
|
|
|
|
'export DEVPLACE_TOOLCHAINS=python,rust,nim,swift' \
|
|
|
|
|
'export PATH=/home/pravda/.local/bin:/opt/rust/cargo/bin:/opt/nim/nimble/bin:/opt/swift/toolchain/usr/bin:$PATH' \
|
|
|
|
|
> /etc/profile.d/devplace-toolchains.sh \
|
|
|
|
|
&& chmod 0644 /etc/profile.d/devplace-toolchains.sh
|
|
|
|
|
|
|
|
|
|
RUN set -eu; \
|
|
|
|
|
for tool in "python --version" "rustc --version" "cargo --version" \
|
Make dev workspaces serve a working browser IDE end to end
The workspace feature shipped its routes, agent tools and docs, but the
editor was never reachable: the project page had no entry point, the ppy
image had no code-server binary, no certificate was ever requested for a
tunnel, and both nginx and the proxy dropped what the editor needs.
- Add a VS Code button to the project action row and a Workspace item to
the overflow menu, gated by can_open_workspace plus a running instance
(viewer_can_workspace and workspace_editor_url on ProjectDetailOut).
- Install a pinned code-server in ppy.Dockerfile before USER pravda and
assert it in the build smoke test, so an image that cannot run the
editor no longer builds green.
- Run the editor with --auth password and a per workspace 8 character
pronounceable secret, minted once at the ensure_editor_password choke
point and injected as PASSWORD. Keep it off WorkspaceViewOut, which the
admin listing shares.
- Publish the editor tunnel when a workspace is created and issue its
certificate from a new WorkspaceService phase against the molohttp admin
API, then notify the owner with the live URL and the password. Only
pending rows are retried, so a broken host cannot burn the ACME failure
rate limit. Renewal stays molohttp's job.
- Forward the original Host on proxied requests and the client cookie on
proxied websockets, so code-server scopes its session cookie to the
public hostname and authenticates the workbench socket.
- Recreate a container stuck in the created state instead of retrying
docker start forever against an image it can no longer run.
- Return a JSON string from WorkspaceController.dispatch; raw dicts landed
in a tool message and aborted the turn at the model endpoint.
- Let the nginx catch-all carry websocket upgrades, keeping upstream
keepalive, so tunnelled apps and the editor both connect.
2026-08-07 13:46:40 +02:00
|
|
|
"nim --version" "nimble --version" "swift --version" \
|
|
|
|
|
"code-server --version"; do \
|
2026-08-07 10:53:08 +02:00
|
|
|
$tool > /tmp/toolcheck 2>&1 || { echo "TOOLCHAIN FAILED: $tool"; cat /tmp/toolcheck; exit 1; }; \
|
|
|
|
|
head -1 /tmp/toolcheck; \
|
|
|
|
|
done; \
|
|
|
|
|
rm -f /tmp/toolcheck; \
|
|
|
|
|
for b in /usr/local/bin/sudo /usr/local/bin/aptroot /usr/bin/pagent.py \
|
Make dev workspaces serve a working browser IDE end to end
The workspace feature shipped its routes, agent tools and docs, but the
editor was never reachable: the project page had no entry point, the ppy
image had no code-server binary, no certificate was ever requested for a
tunnel, and both nginx and the proxy dropped what the editor needs.
- Add a VS Code button to the project action row and a Workspace item to
the overflow menu, gated by can_open_workspace plus a running instance
(viewer_can_workspace and workspace_editor_url on ProjectDetailOut).
- Install a pinned code-server in ppy.Dockerfile before USER pravda and
assert it in the build smoke test, so an image that cannot run the
editor no longer builds green.
- Run the editor with --auth password and a per workspace 8 character
pronounceable secret, minted once at the ensure_editor_password choke
point and injected as PASSWORD. Keep it off WorkspaceViewOut, which the
admin listing shares.
- Publish the editor tunnel when a workspace is created and issue its
certificate from a new WorkspaceService phase against the molohttp admin
API, then notify the owner with the live URL and the password. Only
pending rows are retried, so a broken host cannot burn the ACME failure
rate limit. Renewal stays molohttp's job.
- Forward the original Host on proxied requests and the client cookie on
proxied websockets, so code-server scopes its session cookie to the
public hostname and authenticates the workbench socket.
- Recreate a container stuck in the created state instead of retrying
docker start forever against an image it can no longer run.
- Return a JSON string from WorkspaceController.dispatch; raw dicts landed
in a tool message and aborted the turn at the model endpoint.
- Let the nginx catch-all carry websocket upgrades, keeping upstream
keepalive, so tunnelled apps and the editor both connect.
2026-08-07 13:46:40 +02:00
|
|
|
/usr/bin/botje.py /usr/bin/d.py /usr/bin/dpc /usr/local/bin/code-server; do \
|
2026-08-07 10:53:08 +02:00
|
|
|
[ -x "$b" ] || { echo "missing or not executable: $b"; exit 1; }; \
|
|
|
|
|
done; \
|
|
|
|
|
[ -f /home/pravda/.vimrc ] || { echo "missing /home/pravda/.vimrc"; exit 1; }
|
|
|
|
|
|
2026-08-10 00:23:20 +02:00
|
|
|
RUN set -eu; \
|
|
|
|
|
ext=/usr/local/lib/code-server/lib/vscode/extensions/devplace-workspace; \
|
|
|
|
|
[ -f "$ext/package.json" ] || { echo "missing the DevPlace extension"; exit 1; }; \
|
|
|
|
|
[ -f "$ext/extension.js" ] || { echo "missing the DevPlace extension entry point"; exit 1; }; \
|
|
|
|
|
for theme in devplace-dark devplace-light; do \
|
|
|
|
|
python3 -c "import json,sys; json.load(open('$ext/themes/$theme.json'))" \
|
|
|
|
|
|| { echo "invalid theme: $theme"; exit 1; }; \
|
|
|
|
|
done; \
|
|
|
|
|
python3 -c "import json; d=json.load(open('$ext/package.json')); assert d['contributes']['configurationDefaults']['security.workspace.trust.enabled'] is False, 'trust default lost'"; \
|
|
|
|
|
[ -f /usr/local/lib/code-server/src/browser/media/favicon.svg ] || { echo "missing the DevPlace favicon"; exit 1; }; \
|
|
|
|
|
python3 -c "import json; d=json.load(open('/usr/local/lib/code-server/lib/vscode/product.json')); assert d['nameShort']=='DevPlace', d['nameShort']; assert d['nameLong']=='DevPlace Workspace', d['nameLong']"; \
|
|
|
|
|
grep -q 'devplace-login-theme' /usr/local/lib/code-server/src/browser/pages/login.css \
|
|
|
|
|
|| { echo "the DevPlace login stylesheet was not applied"; exit 1; }; \
|
|
|
|
|
for flag in --app-name --disable-workspace-trust --disable-getting-started-override --welcome-text; do \
|
|
|
|
|
code-server --help 2>&1 | grep -q -- "$flag" \
|
|
|
|
|
|| { echo "code-server no longer supports $flag"; exit 1; }; \
|
|
|
|
|
done; \
|
|
|
|
|
echo "DevPlace branding verified"
|
|
|
|
|
|
2026-06-09 16:06:02 +02:00
|
|
|
CMD ["sleep", "infinity"]
|