feat: add devcontainer for local setup

This commit is contained in:
Abhishek Kumar 2026-05-25 15:58:26 +05:30
parent a725fda274
commit 6b33addb25
26 changed files with 671 additions and 130 deletions

102
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,102 @@
# =============================================================================
# Stage 1: venv-builder
# Minimal image whose only job is to populate the venv. Uses the same Python
# source as the runtime stage (deadsnakes) so the symlinks inside the venv
# (e.g. venv/bin/python -> /usr/bin/python3.13) stay valid after COPY --from.
# Everything in this stage except the venv itself is discarded.
# =============================================================================
FROM ubuntu:24.04 AS venv-builder
RUN apt-get update \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt-get install -y --no-install-recommends \
build-essential \
curl \
ca-certificates \
git \
libpq-dev \
pkg-config \
software-properties-common \
&& add-apt-repository -y ppa:deadsnakes/ppa \
&& apt-get install -y --no-install-recommends \
python3.13 \
python3.13-venv \
python3.13-dev \
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
# Build the venv at the path it will live at in the final image, so shebangs
# and console-scripts inside the venv reference the correct runtime location
# once the seed step rsyncs them into the named volume.
ENV VIRTUAL_ENV=/workspaces/dograh/venv \
PATH=/workspaces/dograh/venv/bin:$PATH
RUN mkdir -p /workspaces/dograh && python3.13 -m venv "$VIRTUAL_ENV"
# Layer 1: API deps. Cache invalidates only when these two files change.
RUN --mount=type=bind,source=api/requirements.txt,target=/tmp/req.txt \
--mount=type=bind,source=api/requirements.dev.txt,target=/tmp/req.dev.txt \
--mount=type=cache,target=/root/.cache/uv \
uv pip install -r /tmp/req.txt -r /tmp/req.dev.txt
# Layer 2: pipecat deps. Cache invalidates when pipecat source changes.
# After installing pipecat, two hardening tweaks (mirrored from api/Dockerfile):
# 1. Swap opencv-python (pulled by pipecat[webrtc]) for opencv-python-headless.
# The non-headless build links against X11/Qt (libxcb*); without those
# shared libs in the image, `import cv2` fails at runtime.
# 2. Pre-download NLTK's punkt_tab tokenizer so pipecat's text processing
# doesn't hit the network on first agent run. NLTK auto-finds it under
# sys.prefix/nltk_data, so it travels with the venv on COPY/rsync.
RUN --mount=type=bind,source=pipecat,target=/tmp/pipecat,rw \
--mount=type=cache,target=/root/.cache/uv \
uv pip install '/tmp/pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb,mcp]' \
&& uv pip install --group /tmp/pipecat/pyproject.toml:dev \
&& uv pip uninstall opencv-python \
&& uv pip install opencv-python-headless \
&& python -c "import nltk; nltk.download('punkt_tab', download_dir='/workspaces/dograh/venv/nltk_data', quiet=True)"
# =============================================================================
# Stage 2: runtime devcontainer image
# Inherits the devcontainer base (vscode user, sudo, etc.) and brings only the
# populated venv across from the builder stage.
# =============================================================================
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
RUN apt-get update \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt-get install -y --no-install-recommends \
build-essential \
curl \
ffmpeg \
git \
jq \
libpq-dev \
pkg-config \
postgresql-client \
procps \
redis-tools \
rsync \
software-properties-common \
&& add-apt-repository -y ppa:deadsnakes/ppa \
&& apt-get install -y --no-install-recommends \
python3.13 \
python3.13-venv \
python3.13-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# uv is still needed at runtime so post-create.sh can do the editable
# pipecat install (and any ad-hoc `uv pip install` users might run).
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
# Bring the populated venv across. At runtime, the named volume in
# docker-compose.yml shadows /workspaces/dograh/venv; post-create.sh
# rsyncs from /opt/venv-template into the (initially empty) volume,
# comparing build-stamps so an image rebuild that changed deps re-seeds.
COPY --from=venv-builder --chown=vscode:vscode /workspaces/dograh/venv /opt/venv-template
RUN date -u +%s > /opt/venv-template/.build-stamp \
&& chown vscode:vscode /opt/venv-template/.build-stamp
ENV VIRTUAL_ENV=/workspaces/dograh/venv \
PATH=/workspaces/dograh/venv/bin:$PATH

View file

@ -0,0 +1,9 @@
{
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "1.7.1",
"resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6",
"integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6"
}
}
}

View file

@ -0,0 +1,71 @@
{
"name": "Dograh",
"dockerComposeFile": [
"../docker-compose-local.yaml",
"docker-compose.yml"
],
"service": "workspace",
"runServices": [
"workspace",
"postgres",
"redis",
"minio"
],
"workspaceFolder": "/workspaces/dograh",
"shutdownAction": "stopCompose",
"overrideCommand": false,
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "24"
}
},
"initializeCommand": "git submodule update --init --recursive",
"postCreateCommand": "bash .devcontainer/scripts/post-create.sh",
"postStartCommand": "bash .devcontainer/scripts/post-start.sh",
"forwardPorts": [
5432,
6379,
9000,
9001
],
"portsAttributes": {
"3000": {
"label": "Dograh UI",
"onAutoForward": "ignore"
},
"8000": {
"label": "Dograh API",
"onAutoForward": "ignore"
},
"5432": {
"label": "Postgres"
},
"6379": {
"label": "Redis"
},
"9000": {
"label": "MinIO API"
},
"9001": {
"label": "MinIO Console"
}
},
"customizations": {
"vscode": {
"settings": {
"python.defaultInterpreterPath": "/workspaces/dograh/venv/bin/python",
"terminal.integrated.defaultProfile.linux": "bash"
},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.debugpy",
"ms-azuretools.vscode-docker",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}
}
}

View file

@ -0,0 +1,31 @@
services:
workspace:
build:
context: .
dockerfile: .devcontainer/Dockerfile
command: sleep infinity
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
environment:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PYTHONUNBUFFERED: "1"
extra_hosts:
- "host.docker.internal:host-gateway"
init: true
networks:
- app-network
volumes:
- .:/workspaces/dograh:cached
- dograh-venv:/workspaces/dograh/venv
- dograh-ui-node_modules:/workspaces/dograh/ui/node_modules
- dograh-ts-validator-node_modules:/workspaces/dograh/api/mcp_server/ts_validator/node_modules
volumes:
dograh-venv:
dograh-ui-node_modules:
dograh-ts-validator-node_modules:

View file

@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="/workspaces/dograh"
UI_ENV_EXAMPLE="$ROOT_DIR/ui/.env.example"
UI_ENV_FILE="$ROOT_DIR/ui/.env"
VENV_PATH="$ROOT_DIR/venv"
VENV_TEMPLATE="/opt/venv-template"
TOTAL_STEPS=5
STEP=0
STEP_START=$SECONDS
SCRIPT_START=$SECONDS
step() {
STEP=$((STEP + 1))
STEP_START=$SECONDS
printf '\n==> [%d/%d] %s\n' "$STEP" "$TOTAL_STEPS" "$1"
}
step_done() {
printf ' done in %ds\n' "$((SECONDS - STEP_START))"
}
fail() {
printf '\n!! FAILED at step %d/%d (%s) after %ds\n' \
"$STEP" "$TOTAL_STEPS" "${1:-unknown}" "$((SECONDS - SCRIPT_START))" >&2
exit 1
}
trap 'fail "exit $?"' ERR
copy_if_missing() {
local src=$1
local dst=$2
if [[ -f "$dst" ]]; then
echo "Keeping existing $dst"
return
fi
cp "$src" "$dst"
echo "Created $dst from $src"
}
# Copy an api/.env*.example template to its target, rewriting infra hostnames
# from `localhost` to the docker service names defined in
# docker-compose-local.yaml. MINIO_PUBLIC_ENDPOINT stays on localhost — that
# URL ends up in UI responses and is loaded by the host browser via the
# forwarded port. No-op if the target already exists.
copy_env_with_docker_hostnames() {
local src=$1
local dst=$2
if [[ -f "$dst" ]]; then
echo "Keeping existing $dst"
return
fi
cp "$src" "$dst"
sed -i \
-e 's|@localhost:5432|@postgres:5432|g' \
-e 's|@localhost:6379|@redis:6379|g' \
-e 's|^MINIO_ENDPOINT=localhost:9000|MINIO_ENDPOINT=minio:9000|' \
"$dst"
echo "Created $dst from $src (rewrote service hostnames for docker network)"
}
# Seed the venv named volume from the image-baked template, but only when
# the template's build-stamp differs from what's currently in the volume
# (first start, or any rebuild that changed requirements.txt / pipecat).
seed_venv() {
local image_stamp venv_stamp
image_stamp=$(cat "$VENV_TEMPLATE/.build-stamp" 2>/dev/null || echo missing)
venv_stamp=$(cat "$VENV_PATH/.build-stamp" 2>/dev/null || echo none)
if [[ "$image_stamp" == "$venv_stamp" ]]; then
echo "Venv already in sync with image template (stamp=$venv_stamp)"
return
fi
echo "Re-seeding venv: image=$image_stamp, volume=$venv_stamp"
rsync -a --delete "$VENV_TEMPLATE/" "$VENV_PATH/"
}
cd "$ROOT_DIR"
step "Fixing ownership of named volume mountpoints"
# Named volumes are created owned by root; postCreateCommand runs as the
# remote user. Chown the mountpoint roots so the steps below can write.
sudo chown "$(id -u):$(id -g)" \
"$VENV_PATH" \
"$ROOT_DIR/ui/node_modules" \
"$ROOT_DIR/api/mcp_server/ts_validator/node_modules"
step_done
step "Seeding venv from image template"
seed_venv
step_done
step "Copying example env files into place"
copy_env_with_docker_hostnames "$ROOT_DIR/api/.env.example" "$ROOT_DIR/api/.env"
copy_env_with_docker_hostnames "$ROOT_DIR/api/.env.test.example" "$ROOT_DIR/api/.env.test"
copy_if_missing "$UI_ENV_EXAMPLE" "$UI_ENV_FILE"
step_done
step "Switching pipecat to editable install from workspace"
# pipecat's deps are already in the seeded venv as a frozen snapshot from
# the build context. Re-register editable from the bind-mounted workspace
# so source edits take effect. --no-deps skips re-resolving transitive
# dependencies (already present from the seeded image template).
uv pip install -e "$ROOT_DIR/pipecat" --no-deps
step_done
step "Installing npm dependencies (ui + ts_validator in parallel)"
npm ci --prefix ui &
ui_pid=$!
npm ci --prefix api/mcp_server/ts_validator &
ts_pid=$!
wait "$ui_pid" || fail "npm ci ui"
wait "$ts_pid" || fail "npm ci ts_validator"
step_done
printf '\nDevcontainer bootstrap complete in %ds.\n' "$((SECONDS - SCRIPT_START))"

View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
# Intentionally no `http://localhost:PORT` URLs below — VS Code's terminal
# URL detector adds any printed URL to its auto-forwarded-ports list and
# then polls it, which produces ECONNREFUSED log spam every ~20s for ports
# that aren't bound yet. The Ports panel auto-detects bound ports anyway.
cat <<'EOF'
Dograh devcontainer ready.
Start the backend:
bash scripts/start_services_dev.sh
Start the UI in another terminal:
cd ui && npm run dev -- --hostname 0.0.0.0
URLs and other workflow notes: docs/contribution/setup.mdx
EOF