mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Refactor dev environment setup with auto-detection and Docker support (#18564)
## Summary Completely rewrites the development environment setup script to be more robust, idempotent, and flexible. The new implementation auto-detects available services (local PostgreSQL/Redis vs Docker), provides multiple operational modes, and includes comprehensive health checks and error handling. ## Key Changes - **Enhanced setup script** (`packages/twenty-utils/setup-dev-env.sh`): - Added auto-detection logic to prefer local services (PostgreSQL 16, Redis) over Docker - Implemented service health checks with retry logic (30s timeout) - Added command-line flags: `--docker` (force Docker), `--down` (stop services), `--reset` (wipe data) - Improved error handling with `set -euo pipefail` and descriptive failure messages - Added helper functions for service detection, startup, and status checking - Fallback to manual `.env` file copying if Nx is unavailable - Enhanced output with clear status messages and usage instructions - **New Docker Compose file** (`packages/twenty-docker/docker-compose.dev.yml`): - Dedicated development infrastructure file (PostgreSQL 16 + Redis 7) - Includes health checks for both services - Configured with appropriate restart policies and volume management - Separate from production compose configuration - **Updated documentation** (`CLAUDE.md`): - Clarified that all environments (CI, local, Claude Code, Cursor) use the same setup script - Documented new command-line flags and their purposes - Noted that CI workflows manage services independently via GitHub Actions - **Updated Cursor environment config** (`.cursor/environment.json`): - Simplified to use the new unified setup script instead of complex inline commands ## Implementation Details The script now follows a clear three-phase approach: 1. **Service startup** — Auto-detects and starts PostgreSQL and Redis (local or Docker) 2. **Database creation** — Creates 'default' and 'test' databases 3. **Environment configuration** — Sets up `.env` files via Nx or direct file copy The auto-detection logic prioritizes local services for better performance while gracefully falling back to Docker if local services aren't available. All operations are idempotent and safe to run multiple times. https://claude.ai/code/session_01UDxa2Kp1ub9tTL3pnpBVFs --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
15d0970f72
commit
f262437da6
5 changed files with 301 additions and 29 deletions
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Setting up Docker Compose environment...' && cd packages/twenty-docker && cp -n docker-compose.yml docker-compose.dev.yml || true && echo 'Dependencies installed and docker-compose prepared'",
|
||||
"start": "sudo service docker start && echo 'Docker service started' && cd packages/twenty-docker && echo 'Installing yq for YAML processing...' && sudo apt-get update -qq && sudo apt-get install -y wget && wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq && echo 'Patching docker-compose for local development...' && yq eval 'del(.services.server.image)' -i docker-compose.dev.yml && yq eval '.services.server.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.server.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && yq eval 'del(.services.worker.image)' -i docker-compose.dev.yml && yq eval '.services.worker.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.worker.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && echo 'Setting up .env file with database configuration...' && echo 'SERVER_URL=http://localhost:3000' > .env && echo 'APP_SECRET='$(openssl rand -base64 32) >> .env && echo 'PG_DATABASE_PASSWORD='$(openssl rand -hex 16) >> .env && echo 'PG_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres' >> .env && echo 'SIGN_IN_PREFILLED=true' >> .env && echo 'Building and starting services...' && docker-compose -f docker-compose.dev.yml up -d --build && echo 'Waiting for services to initialize...' && sleep 30 && echo 'Checking service health...' && docker-compose -f docker-compose.dev.yml ps && echo 'Environment setup complete!'",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Database Setup & Seed",
|
||||
"command": "sleep 40 && cd packages/twenty-docker && echo 'Waiting for PostgreSQL to be ready...' && until docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres; do echo 'Waiting for PostgreSQL...'; sleep 5; done && echo 'PostgreSQL is ready!' && echo 'Waiting for Twenty server to be healthy...' && until docker-compose -f docker-compose.dev.yml exec -T server curl --fail http://localhost:3000/healthz 2>/dev/null; do echo 'Waiting for server...'; sleep 5; done && echo 'Server is healthy!' && echo 'Running database setup and seeding...' && docker-compose -f docker-compose.dev.yml exec -T server npx nx database:reset twenty-server && echo 'Database seeded successfully!' && bash"
|
||||
},
|
||||
{
|
||||
"name": "Application Logs",
|
||||
"command": "sleep 35 && cd packages/twenty-docker && echo 'Following application logs...' && docker-compose -f docker-compose.dev.yml logs -f server worker"
|
||||
},
|
||||
{
|
||||
"name": "Service Monitor",
|
||||
"command": "sleep 15 && cd packages/twenty-docker && echo '=== Service Status Monitor ===' && while true; do clear; echo '=== Service Status at $(date) ===' && docker-compose -f docker-compose.dev.yml ps && echo '\\n=== Health Status ===' && (docker-compose -f docker-compose.dev.yml exec -T server curl -s http://localhost:3000/healthz 2>/dev/null && echo '✅ Twenty Server: Healthy') || echo '❌ Twenty Server: Not Ready' && (docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres 2>/dev/null && echo '✅ PostgreSQL: Ready') || echo '❌ PostgreSQL: Not Ready' && echo '\\n=== Database Connection Test ===' && docker-compose -f docker-compose.dev.yml exec -T server node -e \"const { Client } = require('pg'); const client = new Client({connectionString: process.env.PG_DATABASE_URL}); client.connect().then(() => {console.log('✅ Database Connection: OK'); client.end();}).catch(e => console.log('❌ Database Connection: Failed -', e.message));\" || echo 'Connection test failed' && sleep 45; done"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"install": "yarn install",
|
||||
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"start": "(sudo service docker start || service docker start || true) && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Development Server",
|
||||
|
|
|
|||
17
CLAUDE.md
17
CLAUDE.md
|
|
@ -188,13 +188,22 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
|||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## CI Environment (GitHub Actions)
|
||||
## Dev Environment Setup
|
||||
|
||||
When running in CI, the dev environment is **not** pre-configured. Dependencies are installed but builds, env files, and databases are not set up.
|
||||
All dev environments (Claude Code web, Cursor, local) use one script:
|
||||
|
||||
- **Before running tests, builds, lint, type checks, or DB operations**, run: `bash packages/twenty-utils/setup-dev-env.sh`
|
||||
```bash
|
||||
bash packages/twenty-utils/setup-dev-env.sh
|
||||
```
|
||||
|
||||
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
|
||||
|
||||
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
|
||||
- `--down` — stop services
|
||||
- `--reset` — wipe data and restart fresh
|
||||
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
|
||||
- The script is idempotent and safe to run multiple times.
|
||||
|
||||
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
|
||||
|
||||
## Important Files
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
|
|
|
|||
42
packages/twenty-docker/docker-compose.dev.yml
Normal file
42
packages/twenty-docker/docker-compose.dev.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Development infrastructure services only (Postgres + Redis).
|
||||
# Use this when developing locally against the source code.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.dev.yml up -d
|
||||
# docker compose -f docker-compose.dev.yml down # stop
|
||||
# docker compose -f docker-compose.dev.yml down -v # stop + wipe data
|
||||
|
||||
name: twenty-dev
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- dev-db-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: default
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
dev-db-data:
|
||||
|
|
@ -1,12 +1,251 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
# =============================================================================
|
||||
# Twenty CRM — Development Environment Setup
|
||||
# =============================================================================
|
||||
# Single entry point for setting up a dev environment. Idempotent.
|
||||
#
|
||||
# What it does:
|
||||
# 1. Starts Postgres + Redis (local services or Docker, auto-detected)
|
||||
# 2. Creates 'default' and 'test' databases
|
||||
# 3. Copies .env.example -> .env for front and server
|
||||
#
|
||||
# Usage (from repo root):
|
||||
# bash packages/twenty-utils/setup-dev-env.sh # start + configure
|
||||
# bash packages/twenty-utils/setup-dev-env.sh --down # stop services
|
||||
# bash packages/twenty-utils/setup-dev-env.sh --reset # wipe data + restart
|
||||
# bash packages/twenty-utils/setup-dev-env.sh --docker # force Docker mode
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
echo "Setting up dev environment..."
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
COMPOSE_FILE="$REPO_ROOT/packages/twenty-docker/docker-compose.dev.yml"
|
||||
|
||||
npx nx reset:env twenty-front
|
||||
npx nx reset:env twenty-server
|
||||
info() { echo "=> $*"; }
|
||||
ok() { echo " done: $*"; }
|
||||
fail() { echo " FAIL: $*" >&2; }
|
||||
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";' 2>/dev/null || true
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";' 2>/dev/null || true
|
||||
# --------------- detection helpers ---------------
|
||||
has_local_pg() {
|
||||
command -v pg_ctlcluster &>/dev/null && pg_lsclusters 2>/dev/null | grep -q "16"
|
||||
}
|
||||
|
||||
has_local_redis() {
|
||||
command -v redis-server &>/dev/null
|
||||
}
|
||||
|
||||
can_use_docker() {
|
||||
docker compose version &>/dev/null 2>&1
|
||||
}
|
||||
|
||||
pg_is_up() {
|
||||
if command -v pg_isready &>/dev/null; then
|
||||
pg_isready -h localhost -p 5432 -U postgres -q 2>/dev/null
|
||||
elif command -v psql &>/dev/null; then
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -c "SELECT 1" &>/dev/null
|
||||
elif can_use_docker && docker compose -f "$COMPOSE_FILE" ps --quiet db 2>/dev/null | grep -q .; then
|
||||
docker compose -f "$COMPOSE_FILE" exec -T db pg_isready -U postgres -q 2>/dev/null
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
redis_is_up() {
|
||||
if command -v redis-cli &>/dev/null; then
|
||||
redis-cli -h localhost -p 6379 ping 2>/dev/null | grep -q PONG
|
||||
elif can_use_docker && docker compose -f "$COMPOSE_FILE" ps --quiet redis 2>/dev/null | grep -q .; then
|
||||
docker compose -f "$COMPOSE_FILE" exec -T redis redis-cli ping 2>/dev/null | grep -q PONG
|
||||
else
|
||||
# Portable fallback using bash /dev/tcp (no nc -q dependency)
|
||||
timeout 2 bash -c 'exec 3<>/dev/tcp/localhost/6379; echo PING >&3; read -r reply <&3; exec 3>&-; echo "$reply"' 2>/dev/null | grep -q PONG
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_pg() {
|
||||
local retries=30
|
||||
while ! pg_is_up; do
|
||||
retries=$((retries - 1))
|
||||
if [ "$retries" -le 0 ]; then fail "PostgreSQL did not start in time"; exit 1; fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_redis() {
|
||||
local retries=30
|
||||
while ! redis_is_up; do
|
||||
retries=$((retries - 1))
|
||||
if [ "$retries" -le 0 ]; then fail "Redis did not start in time"; exit 1; fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# --------------- parse flags ---------------
|
||||
USE_DOCKER=false
|
||||
ACTION="up"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--docker) USE_DOCKER=true ;;
|
||||
--down) ACTION="down" ;;
|
||||
--reset) ACTION="reset" ;;
|
||||
*) echo "Unknown flag: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# --------------- stop ---------------
|
||||
stop_docker() {
|
||||
if can_use_docker && docker compose -f "$COMPOSE_FILE" ps -a --quiet 2>/dev/null | grep -q .; then
|
||||
docker compose -f "$COMPOSE_FILE" down "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
stop_local() {
|
||||
if has_local_pg; then sudo pg_ctlcluster 16 main stop 2>/dev/null || true; fi
|
||||
if has_local_redis && pgrep -x redis-server &>/dev/null; then
|
||||
sudo service redis-server stop 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
stop_services() {
|
||||
if [ "$USE_DOCKER" = true ]; then
|
||||
stop_docker "$@"
|
||||
else
|
||||
stop_docker "$@"
|
||||
stop_local
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$ACTION" = "down" ]; then
|
||||
info "Stopping dev services..."
|
||||
stop_services
|
||||
ok "Services stopped"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$ACTION" = "reset" ]; then
|
||||
info "Resetting dev services (wiping data)..."
|
||||
# Wipe local Redis data while it's still running
|
||||
if [ "$USE_DOCKER" = false ] && has_local_redis && pgrep -x redis-server &>/dev/null; then
|
||||
info "Flushing local Redis data..."
|
||||
redis-cli flushall 2>/dev/null || true
|
||||
fi
|
||||
# Wipe local PostgreSQL data while it's still running
|
||||
if [ "$USE_DOCKER" = false ] && has_local_pg; then
|
||||
info "Dropping local databases..."
|
||||
sudo pg_ctlcluster 16 main start 2>/dev/null || true
|
||||
wait_for_pg
|
||||
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS "default";' 2>/dev/null || true
|
||||
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS "test";' 2>/dev/null || true
|
||||
fi
|
||||
# Stop Docker with -v to remove volumes
|
||||
stop_docker -v 2>/dev/null || stop_docker
|
||||
# Stop local services
|
||||
if [ "$USE_DOCKER" = false ]; then
|
||||
stop_local
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 1. Start services (auto-detect: local > Docker)
|
||||
# =============================================================================
|
||||
start_pg() {
|
||||
if pg_is_up; then
|
||||
ok "PostgreSQL already running"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$USE_DOCKER" = false ] && has_local_pg; then
|
||||
info "Starting local PostgreSQL..."
|
||||
sudo pg_ctlcluster 16 main start
|
||||
wait_for_pg
|
||||
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" 2>/dev/null || true
|
||||
elif can_use_docker; then
|
||||
info "Starting PostgreSQL via Docker..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d db
|
||||
wait_for_pg
|
||||
else
|
||||
fail "No PostgreSQL available. Install PostgreSQL 16 or Docker."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
start_redis() {
|
||||
if redis_is_up; then
|
||||
ok "Redis already running"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$USE_DOCKER" = false ] && has_local_redis; then
|
||||
info "Starting local Redis..."
|
||||
sudo service redis-server start 2>/dev/null || redis-server --daemonize yes 2>/dev/null || true
|
||||
wait_for_redis
|
||||
elif can_use_docker; then
|
||||
info "Starting Redis via Docker..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d redis
|
||||
wait_for_redis
|
||||
else
|
||||
fail "No Redis available. Install Redis or Docker."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$USE_DOCKER" = true ]; then
|
||||
info "Starting services via Docker Compose..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d
|
||||
wait_for_pg
|
||||
wait_for_redis
|
||||
else
|
||||
start_pg
|
||||
start_redis
|
||||
fi
|
||||
|
||||
ok "PostgreSQL on localhost:5432"
|
||||
ok "Redis on localhost:6379"
|
||||
|
||||
# =============================================================================
|
||||
# 2. Create databases
|
||||
# =============================================================================
|
||||
info "Creating databases..."
|
||||
run_psql() {
|
||||
if command -v psql &>/dev/null; then
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c "$1" 2>/dev/null || true
|
||||
elif can_use_docker && docker compose -f "$COMPOSE_FILE" ps --quiet db 2>/dev/null | grep -q .; then
|
||||
docker compose -f "$COMPOSE_FILE" exec -T db psql -U postgres -d postgres -c "$1" 2>/dev/null || true
|
||||
else
|
||||
fail "No psql client available and no Docker db container running"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
run_psql 'CREATE DATABASE "default";'
|
||||
run_psql 'CREATE DATABASE "test";'
|
||||
ok "Databases 'default' and 'test' ready"
|
||||
|
||||
# =============================================================================
|
||||
# 3. Environment files (via Nx when available, fallback to cp)
|
||||
# =============================================================================
|
||||
info "Setting up .env files..."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if command -v npx &>/dev/null && [ -d node_modules ]; then
|
||||
npx nx reset:env twenty-front
|
||||
npx nx reset:env twenty-server
|
||||
else
|
||||
for pkg in twenty-front twenty-server; do
|
||||
src="packages/$pkg/.env.example"
|
||||
dst="packages/$pkg/.env"
|
||||
if [ -f "$src" ] && [ ! -f "$dst" ]; then
|
||||
cp "$src" "$dst"
|
||||
ok "$pkg/.env created"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
echo ""
|
||||
echo "Dev environment ready."
|
||||
echo ""
|
||||
echo " yarn start # start everything"
|
||||
echo " npx nx start twenty-front # frontend -> http://localhost:3001"
|
||||
echo " npx nx start twenty-server # backend -> http://localhost:3000"
|
||||
echo ""
|
||||
|
|
|
|||
Loading…
Reference in a new issue