From 848269a4d4df7349acfc861ff926b17fe4c4a548 Mon Sep 17 00:00:00 2001 From: M0N7Y5 <17201053+M0n7y5@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:24:48 +0100 Subject: [PATCH] Added support for developing in devcontainer (#480) --- .devcontainer/Caddyfile.dev | 54 ++++++++++++++++++++ .devcontainer/Dockerfile | 40 +++++++++++++++ .devcontainer/devcontainer.json | 75 +++++++++++++++++++++++++++ .devcontainer/docker-compose.yml | 64 +++++++++++++++++++++++ .devcontainer/livekit.yaml | 30 +++++++++++ .devcontainer/on-create.sh | 70 ++++++++++++++++++++++++++ .devcontainer/process-compose.yml | 57 +++++++++++++++++++++ .vscode/launch.json | 84 +++++++++++++++++++++++++++++++ README.md | 16 +++++- 9 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Caddyfile.dev create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .devcontainer/livekit.yaml create mode 100755 .devcontainer/on-create.sh create mode 100644 .devcontainer/process-compose.yml create mode 100644 .vscode/launch.json diff --git a/.devcontainer/Caddyfile.dev b/.devcontainer/Caddyfile.dev new file mode 100644 index 00000000..9be057a7 --- /dev/null +++ b/.devcontainer/Caddyfile.dev @@ -0,0 +1,54 @@ +# Like dev/Caddyfile.dev, but LiveKit and Mailpit are referenced by their +# Docker Compose hostnames instead of 127.0.0.1. + +{ + auto_https off + admin off +} + +:48763 { + handle /_caddy_health { + respond "OK" 200 + } + + @gateway path /gateway /gateway/* + handle @gateway { + uri strip_prefix /gateway + reverse_proxy 127.0.0.1:49107 + } + + @marketing path /marketing /marketing/* + handle @marketing { + uri strip_prefix /marketing + reverse_proxy 127.0.0.1:49531 + } + + @server path /admin /admin/* /api /api/* /s3 /s3/* /queue /queue/* /media /media/* /_health /_ready /_live /.well-known/fluxer + handle @server { + reverse_proxy 127.0.0.1:49319 + } + + @livekit path /livekit /livekit/* + handle @livekit { + uri strip_prefix /livekit + reverse_proxy livekit:7880 + } + + redir /mailpit /mailpit/ + handle_path /mailpit/* { + rewrite * /mailpit{path} + reverse_proxy mailpit:8025 + } + + handle { + reverse_proxy 127.0.0.1:49427 { + header_up Connection {http.request.header.Connection} + header_up Upgrade {http.request.header.Upgrade} + } + } + + log { + output stdout + format console + } +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..16c45417 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,40 @@ +# Language runtimes (Node.js, Go, Rust, Python) are installed via devcontainer +# features. This Dockerfile handles Erlang/OTP (no feature available) and +# tools like Caddy, process-compose, rebar3, uv, ffmpeg, and exiftool. + +FROM erlang:28-slim AS erlang + +FROM mcr.microsoft.com/devcontainers/base:debian-13 + +ARG DEBIAN_FRONTEND=noninteractive +ARG REBAR3_VERSION=3.24.0 +ARG PROCESS_COMPOSE_VERSION=1.90.0 + +# Both erlang:28-slim and debian-13 are Trixie-based, so OpenSSL versions match. +COPY --from=erlang /usr/local/lib/erlang /usr/local/lib/erlang +RUN ln -sf /usr/local/lib/erlang/bin/* /usr/local/bin/ + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libncurses6 libsctp1 \ + build-essential pkg-config \ + ffmpeg libimage-exiftool-perl \ + sqlite3 libsqlite3-dev \ + libssl-dev openssl \ + gettext-base lsof iproute2 \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL "https://github.com/erlang/rebar3/releases/download/${REBAR3_VERSION}/rebar3" \ + -o /usr/local/bin/rebar3 \ + && chmod +x /usr/local/bin/rebar3 + +RUN curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=amd64" \ + -o /usr/local/bin/caddy \ + && chmod +x /usr/local/bin/caddy + +RUN curl -fsSL "https://github.com/F1bonacc1/process-compose/releases/download/v${PROCESS_COMPOSE_VERSION}/process-compose_linux_amd64.tar.gz" \ + | tar xz -C /usr/local/bin process-compose \ + && chmod +x /usr/local/bin/process-compose + +RUN curl -fsSL "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-gnu.tar.gz" \ + | tar xz --strip-components=1 -C /usr/local/bin \ + && chmod +x /usr/local/bin/uv /usr/local/bin/uvx diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..2f341485 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,75 @@ +{ + "name": "Fluxer", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "24", + "pnpmVersion": "10.29.3" + }, + "ghcr.io/devcontainers/features/go:1": { + "version": "1.24" + }, + "ghcr.io/devcontainers/features/rust:1": { + "version": "1.93.0", + "targets": "wasm32-unknown-unknown" + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "os-provided", + "installTools": false + } + }, + + "onCreateCommand": ".devcontainer/on-create.sh", + + "remoteEnv": { + "FLUXER_CONFIG": "${containerWorkspaceFolder}/config/config.json", + "FLUXER_DATABASE": "sqlite" + }, + + "forwardPorts": [48763, 6379, 7700, 7880], + + "portsAttributes": { + "48763": { + "label": "Fluxer (Caddy)", + "onAutoForward": "openBrowser", + "protocol": "http" + }, + "6379": { + "label": "Valkey", + "onAutoForward": "silent" + }, + "7700": { + "label": "Meilisearch", + "onAutoForward": "silent" + }, + "7880": { + "label": "LiveKit", + "onAutoForward": "silent" + }, + "9229": { + "label": "Node.js Debugger", + "onAutoForward": "silent" + } + }, + + "customizations": { + "vscode": { + "extensions": [ + "TypeScriptTeam.native-preview", + "biomejs.biome", + "clinyong.vscode-css-modules", + "pgourlain.erlang", + "golang.go", + "rust-lang.rust-analyzer" + ], + "settings": { + "typescript.preferences.includePackageJsonAutoImports": "auto", + "typescript.suggest.autoImports": true, + "typescript.experimental.useTsgo": true + } + } + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..0e009225 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,64 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ..:/workspace:cached + command: sleep infinity + + valkey: + image: valkey/valkey:8-alpine + restart: unless-stopped + command: ['valkey-server', '--appendonly', 'yes', '--save', '60', '1', '--loglevel', 'warning'] + volumes: + - valkey-data:/data + healthcheck: + test: ['CMD', 'valkey-cli', 'ping'] + interval: 10s + timeout: 5s + retries: 5 + + meilisearch: + image: getmeili/meilisearch:v1.14 + restart: unless-stopped + environment: + MEILI_NO_ANALYTICS: 'true' + MEILI_ENV: development + MEILI_MASTER_KEY: fluxer-devcontainer-meili-master-key + volumes: + - meilisearch-data:/meili_data + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:7700/health'] + interval: 10s + timeout: 5s + retries: 5 + + livekit: + image: livekit/livekit-server:v1.9 + restart: unless-stopped + command: --config /etc/livekit.yaml + volumes: + - ./livekit.yaml:/etc/livekit.yaml:ro + + mailpit: + image: axllent/mailpit:latest + restart: unless-stopped + command: ['--webroot', '/mailpit/'] + + nats-core: + image: nats:2-alpine + restart: unless-stopped + command: ['--port', '4222'] + + nats-jetstream: + image: nats:2-alpine + restart: unless-stopped + command: ['--port', '4223', '--jetstream', '--store_dir', '/data'] + volumes: + - nats-jetstream-data:/data + +volumes: + valkey-data: + meilisearch-data: + nats-jetstream-data: diff --git a/.devcontainer/livekit.yaml b/.devcontainer/livekit.yaml new file mode 100644 index 00000000..f7fd2ffd --- /dev/null +++ b/.devcontainer/livekit.yaml @@ -0,0 +1,30 @@ +# Credentials here must match the values on-create.sh writes to config.json. + +port: 7880 + +keys: + fluxer-devcontainer-key: fluxer-devcontainer-secret-key-00000000 + +rtc: + tcp_port: 7881 + port_range_start: 50000 + port_range_end: 50100 + use_external_ip: false + node_ip: 127.0.0.1 + +turn: + enabled: true + domain: localhost + udp_port: 3478 + +webhook: + api_key: fluxer-devcontainer-key + urls: + - http://app:49319/api/webhooks/livekit + +room: + auto_create: true + max_participants: 100 + empty_timeout: 300 + +development: true diff --git a/.devcontainer/on-create.sh b/.devcontainer/on-create.sh new file mode 100755 index 00000000..2c242b02 --- /dev/null +++ b/.devcontainer/on-create.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# Runs once when the container is first created. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +export FLUXER_CONFIG="${FLUXER_CONFIG:-$REPO_ROOT/config/config.json}" + +GREEN='\033[0;32m' +NC='\033[0m' +info() { printf "%b\n" "${GREEN}[devcontainer]${NC} $1"; } + +info "Installing pnpm dependencies..." +pnpm install + +# Codegen outputs (e.g. MasterZodSchema.generated.tsx) are gitignored. +info "Generating config schema..." +pnpm --filter @fluxer/config generate + +if [ ! -f "$FLUXER_CONFIG" ]; then + info "Creating config from development template..." + cp "$REPO_ROOT/config/config.dev.template.json" "$FLUXER_CONFIG" +fi + +# Point services at Docker Compose hostnames and adjust settings that differ +# from the default dev template. +info "Patching config for Docker Compose networking..." +jq ' + # rspack defaults public_scheme to "https" when unset + .domain.public_scheme = "http" | + # Relative path so the app works on any hostname (localhost, 127.0.0.1, etc.) + .app_public.bootstrap_api_endpoint = "/api" | + + .internal.kv = "redis://valkey:6379/0" | + + .integrations.search.url = "http://meilisearch:7700" | + .integrations.search.api_key = "fluxer-devcontainer-meili-master-key" | + + # Credentials must match .devcontainer/livekit.yaml + .integrations.voice.url = "ws://livekit:7880" | + .integrations.voice.webhook_url = "http://app:49319/api/webhooks/livekit" | + .integrations.voice.api_key = "fluxer-devcontainer-key" | + .integrations.voice.api_secret = "fluxer-devcontainer-secret-key-00000000" | + + .integrations.email.smtp.host = "mailpit" | + .integrations.email.smtp.port = 1025 | + + .services.nats.core_url = "nats://nats-core:4222" | + .services.nats.jetstream_url = "nats://nats-jetstream:4223" | + + # Bluesky OAuth requires HTTPS + loopback IPs (RFC 8252), incompatible with + # the HTTP-only devcontainer setup. + .auth.bluesky.enabled = false +' "$FLUXER_CONFIG" > "$FLUXER_CONFIG.tmp" && mv "$FLUXER_CONFIG.tmp" "$FLUXER_CONFIG" + +info "Running bootstrap..." +"$REPO_ROOT/scripts/dev_bootstrap.sh" + +info "Pre-compiling Erlang gateway dependencies..." +(cd "$REPO_ROOT/fluxer_gateway" && rebar3 compile) || { + info "Gateway pre-compilation failed (non-fatal, will compile on first start)" +} + +info "Devcontainer setup complete." +info "" +info " Start all dev processes: process-compose -f .devcontainer/process-compose.yml up" +info " Open the app: http://127.0.0.1:48763" +info " Dev email inbox: http://127.0.0.1:48763/mailpit/" +info "" diff --git a/.devcontainer/process-compose.yml b/.devcontainer/process-compose.yml new file mode 100644 index 00000000..ca4a1361 --- /dev/null +++ b/.devcontainer/process-compose.yml @@ -0,0 +1,57 @@ +# Application processes only — backing services (Valkey, Meilisearch, LiveKit, +# Mailpit, NATS) run via Docker Compose. +# process-compose -f .devcontainer/process-compose.yml up + +is_tui_disabled: false +log_level: info +log_configuration: + flush_each_line: true + +processes: + caddy: + command: caddy run --config .devcontainer/Caddyfile.dev --adapter caddyfile + log_location: dev/logs/caddy.log + readiness_probe: + http_get: + host: 127.0.0.1 + port: 48763 + path: /_caddy_health + availability: + restart: always + + fluxer_server: + command: pnpm --filter fluxer_server dev + log_location: dev/logs/fluxer_server.log + availability: + restart: always + + fluxer_app: + command: ./scripts/dev_fluxer_app.sh + environment: + - FORCE_COLOR=1 + - FLUXER_APP_DEV_PORT=49427 + log_location: dev/logs/fluxer_app.log + availability: + restart: always + + fluxer_gateway: + command: ./scripts/dev_gateway.sh + environment: + - FLUXER_GATEWAY_NO_SHELL=1 + log_location: dev/logs/fluxer_gateway.log + availability: + restart: always + + marketing_dev: + command: pnpm --filter fluxer_marketing dev + environment: + - FORCE_COLOR=1 + log_location: dev/logs/marketing_dev.log + availability: + restart: always + + css_watch: + command: ./scripts/dev_css_watch.sh + log_location: dev/logs/css_watch.log + availability: + restart: always diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..ac5115f8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,84 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug: fluxer_server", + "program": "${workspaceFolder}/fluxer_server/src/startServer.tsx", + "runtimeArgs": ["--import", "tsx"], + "cwd": "${workspaceFolder}/fluxer_server", + "env": { + "FLUXER_CONFIG": "${workspaceFolder}/config/config.json", + "FLUXER_DATABASE": "sqlite" + }, + "console": "integratedTerminal", + "skipFiles": ["/**", "**/node_modules/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug: fluxer_api (standalone)", + "program": "${workspaceFolder}/fluxer_api/src/AppEntrypoint.tsx", + "runtimeArgs": ["--import", "tsx"], + "cwd": "${workspaceFolder}/fluxer_api", + "env": { + "FLUXER_CONFIG": "${workspaceFolder}/config/config.json", + "FLUXER_DATABASE": "sqlite" + }, + "console": "integratedTerminal", + "skipFiles": ["/**", "**/node_modules/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug: fluxer_marketing", + "program": "${workspaceFolder}/fluxer_marketing/src/index.tsx", + "runtimeArgs": ["--import", "tsx"], + "cwd": "${workspaceFolder}/fluxer_marketing", + "env": { + "FLUXER_CONFIG": "${workspaceFolder}/config/config.json" + }, + "console": "integratedTerminal", + "skipFiles": ["/**", "**/node_modules/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug: fluxer_app (DevServer)", + "program": "${workspaceFolder}/fluxer_app/scripts/DevServer.tsx", + "runtimeArgs": ["--import", "tsx"], + "cwd": "${workspaceFolder}/fluxer_app", + "env": { + "FLUXER_APP_DEV_PORT": "49427", + "FORCE_COLOR": "1" + }, + "console": "integratedTerminal", + "skipFiles": ["/**", "**/node_modules/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug: Test Current File", + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", + "args": ["run", "--no-coverage", "${relativeFile}"], + "autoAttachChildProcesses": true, + "console": "integratedTerminal", + "skipFiles": ["/**", "**/node_modules/**"] + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Node Process", + "port": 9229, + "restart": true, + "skipFiles": ["/**", "**/node_modules/**"] + } + ], + "compounds": [ + { + "name": "Debug: Server + App", + "configurations": ["Debug: fluxer_server", "Debug: fluxer_app (DevServer)"] + } + ] +} diff --git a/README.md b/README.md index 7fda384d..2396d5ea 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ TBD ### Devenv development environment -Fluxer supports development through **devenv** only. It provides a reproducible Nix environment and a single, declarative process manager for the dev stack. If you need a different setup, it is currently unsupported. +Fluxer supports development through **devenv** only. It provides a reproducible Nix environment and a single, declarative process manager for the dev stack. 1. Install Nix and devenv using the [devenv getting started guide](https://devenv.sh/getting-started/). 2. Enter the environment: @@ -108,6 +108,20 @@ If you develop on a remote VM behind Cloudflare Tunnels (or a similar HTTP-only The bootstrap script configures LiveKit automatically based on `domain.base_domain` in your `config.json`. When set to a non-localhost domain, it enables external IP discovery so clients can connect directly for media while signaling continues through the tunnel. +### Devcontainer (experimental) + +There is experimental support for developing in a **VS Code Dev Container** / GitHub Codespace without Nix. The `.devcontainer/` directory provides a Docker Compose setup with all required tooling and backing services. + +```bash +# Inside the dev container, start all processes: +process-compose -f .devcontainer/process-compose.yml up +``` + +Open the app at `http://localhost:48763` and the dev email inbox at `http://localhost:48763/mailpit/`. Predefined VS Code debugging targets are available in `.vscode/launch.json`. + +> [!WARNING] +> Bluesky OAuth is disabled in the devcontainer because it requires HTTPS. All other features work normally. + ### Documentation To develop the documentation site with live preview: