mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
feat: add devcontainer for local setup
This commit is contained in:
parent
a725fda274
commit
6b33addb25
26 changed files with 671 additions and 130 deletions
102
.devcontainer/Dockerfile
Normal file
102
.devcontainer/Dockerfile
Normal 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
|
||||
9
.devcontainer/devcontainer-lock.json
Normal file
9
.devcontainer/devcontainer-lock.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
71
.devcontainer/devcontainer.json
Normal file
71
.devcontainer/devcontainer.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
31
.devcontainer/docker-compose.yml
Normal file
31
.devcontainer/docker-compose.yml
Normal 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:
|
||||
119
.devcontainer/scripts/post-create.sh
Executable file
119
.devcontainer/scripts/post-create.sh
Executable 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))"
|
||||
18
.devcontainer/scripts/post-start.sh
Executable file
18
.devcontainer/scripts/post-start.sh
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue