Files
devplacepy/ppy.Dockerfile
T
retoor 6514261730 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-13 12:59:53 +02:00

179 lines
9.6 KiB
Docker

# 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 \
git openssh-client curl wget vim ack ca-certificates build-essential libpq-dev \
tmux apache2-utils procps htop iftop iotop netcat-openbsd zip unzip \
rsync jq 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 \
&& rm -rf /var/lib/apt/lists/*
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
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
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()); 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))"; \
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
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
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" \
"nim --version" "nimble --version" "swift --version" \
"code-server --version"; do \
$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 \
/usr/bin/botje.py /usr/bin/d.py /usr/bin/dpc /usr/local/bin/code-server; do \
[ -x "$b" ] || { echo "missing or not executable: $b"; exit 1; }; \
done; \
[ -f /home/pravda/.vimrc ] || { echo "missing /home/pravda/.vimrc"; exit 1; }
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"]