179 lines
9.6 KiB
Docker
Raw Normal View History

# 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 \
PIP_DEFAULT_TIMEOUT=120 \
PIP_RETRIES=10 \
DEBIAN_FRONTEND=noninteractive \
PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
RUN apt-get update && apt-get install -y --no-install-recommends \
Route container proxies through the leg that is actually reachable The workspace editor hung for 60s and then 504'd. Three independent faults were stacked behind that one symptom. Reachability: editor_target delegated to proxy_target, which returns CONTAINER_PROXY_HOST plus the published host port and never falls back to the container. From inside the app container that address crosses docker0 into the host INPUT chain, whose policy is DROP with an allow-list that does not include the published port range, so the packet was dropped and the request hung rather than being refused. Measured from the app container: container_ip:8443 answers 302, gateway:20006 is dropped. One shared reachable_target now prefers the direct container leg and falls back to the published port, and editor_target uses tunnel_target as services/containers/CLAUDE.md already required. The same defect affected /p/{slug} ingress and every tunnel, since all three resolved through proxy_target. The recorded measurement that motivated the old order (container_ip times out, gateway connects) no longer holds: make docker-attach puts the app on the instances' bridge network, which is what makes the direct leg work. Duplicate response headers: the forwarding core relayed the upstream Date and Server alongside the ones the serving layer generates, so every proxied response carried two of each. Both are singleton headers and duplicating them is malformed HTTP. Serialization: WorkspaceViewOut declared flag_reason and three sibling strings as str, so a NULL column made the workspace page 500 for JSON clients. Documents the two public hostnames and the devplace.net SSH tunnel, so a future session does not conclude the site is down after pointing curl --resolve at an address the hostname does not resolve to, and adds the layered procedure for diagnosing a production failure. Verified on production with Playwright over both hostnames: the code-server login renders and the workbench loads. Suite: 3345 passed.
2026-08-11 20:03:15 +02:00
git openssh-client curl wget vim ack ca-certificates build-essential libpq-dev \
tmux apache2-utils procps htop iftop iotop netcat-openbsd zip unzip \
Route container proxies through the leg that is actually reachable The workspace editor hung for 60s and then 504'd. Three independent faults were stacked behind that one symptom. Reachability: editor_target delegated to proxy_target, which returns CONTAINER_PROXY_HOST plus the published host port and never falls back to the container. From inside the app container that address crosses docker0 into the host INPUT chain, whose policy is DROP with an allow-list that does not include the published port range, so the packet was dropped and the request hung rather than being refused. Measured from the app container: container_ip:8443 answers 302, gateway:20006 is dropped. One shared reachable_target now prefers the direct container leg and falls back to the published port, and editor_target uses tunnel_target as services/containers/CLAUDE.md already required. The same defect affected /p/{slug} ingress and every tunnel, since all three resolved through proxy_target. The recorded measurement that motivated the old order (container_ip times out, gateway connects) no longer holds: make docker-attach puts the app on the instances' bridge network, which is what makes the direct leg work. Duplicate response headers: the forwarding core relayed the upstream Date and Server alongside the ones the serving layer generates, so every proxied response carried two of each. Both are singleton headers and duplicating them is malformed HTTP. Serialization: WorkspaceViewOut declared flag_reason and three sibling strings as str, so a NULL column made the workspace page 500 for JSON clients. Documents the two public hostnames and the devplace.net SSH tunnel, so a future session does not conclude the site is down after pointing curl --resolve at an address the hostname does not resolve to, and adds the layered procedure for diagnosing a production failure. Verified on production with Playwright over both hostnames: the code-server login renders and the workbench loads. Suite: 3345 passed.
2026-08-11 20:03:15 +02:00
rsync jq fakeroot xz-utils pkg-config \
2026-08-07 10:53:08 +02:00
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 \
&& 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
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 \
h2 celery apscheduler watchfiles \
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; \
Route container proxies through the leg that is actually reachable The workspace editor hung for 60s and then 504'd. Three independent faults were stacked behind that one symptom. Reachability: editor_target delegated to proxy_target, which returns CONTAINER_PROXY_HOST plus the published host port and never falls back to the container. From inside the app container that address crosses docker0 into the host INPUT chain, whose policy is DROP with an allow-list that does not include the published port range, so the packet was dropped and the request hung rather than being refused. Measured from the app container: container_ip:8443 answers 302, gateway:20006 is dropped. One shared reachable_target now prefers the direct container leg and falls back to the published port, and editor_target uses tunnel_target as services/containers/CLAUDE.md already required. The same defect affected /p/{slug} ingress and every tunnel, since all three resolved through proxy_target. The recorded measurement that motivated the old order (container_ip times out, gateway connects) no longer holds: make docker-attach puts the app on the instances' bridge network, which is what makes the direct leg work. Duplicate response headers: the forwarding core relayed the upstream Date and Server alongside the ones the serving layer generates, so every proxied response carried two of each. Both are singleton headers and duplicating them is malformed HTTP. Serialization: WorkspaceViewOut declared flag_reason and three sibling strings as str, so a NULL column made the workspace page 500 for JSON clients. Documents the two public hostnames and the devplace.net SSH tunnel, so a future session does not conclude the site is down after pointing curl --resolve at an address the hostname does not resolve to, and adds the layered procedure for diagnosing a production failure. Verified on production with Playwright over both hostnames: the code-server login renders and the workbench loads. Suite: 3345 passed.
2026-08-11 20:03:15 +02:00
python3 -c "import json,pathlib; p=pathlib.Path('/usr/local/lib/code-server/lib/vscode/product.json'); d=json.loads(p.read_text()); patch=json.loads(pathlib.Path('/tmp/product.patch.json').read_text()); d.update({k: ({**d[k], **v} if isinstance(v, dict) and isinstance(d.get(k), dict) else v) for k, v in patch.items()}); p.write_text(json.dumps(d, indent=2))"; \
2026-08-10 00:23:20 +02:00
rm -f /tmp/devplace-login.css /tmp/product.patch.json
COPY sudo /usr/local/bin/sudo
COPY aptroot /usr/local/bin/aptroot
COPY pagent /usr/bin/pagent.py
COPY bot.py /usr/bin/botje.py
COPY d.py /usr/bin/d.py
COPY dpc /usr/bin/dpc
COPY .vimrc /home/pravda/.vimrc
RUN set -eu; \
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; \
ln -sf /usr/local/bin/sudo /usr/bin/sudo || true; \
for t in apt apt-get dpkg; do ln -sf /usr/local/bin/aptroot "/usr/local/bin/$t"; done; \
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; \
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 \
[ -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
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"
CMD ["sleep", "infinity"]