Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ yarn setup:dev
# - Install all dependencies
# - Build Docker executor containers
# - Start development environment with Docker Compose
# - Auto-shift host ports if default ports are busy (e.g. 27017 -> 27018)
# - Mount source code for hot-reload (changes auto-reload containers)
# - Seed database with sample data
# - Start both API server and worker with live reloading
Expand Down Expand Up @@ -255,6 +256,16 @@ yarn docker:clean:all # Complete Docker cleanup
yarn build:executors # Rebuild executor images
```

**Port Already In Use (`address already in use`)**:

```bash
# Retry startup; SafeExec now auto-selects next free host ports
yarn docker:dev

# Example: if 27017 is busy, MongoDB may use 27018
# The selected ports are printed in startup logs
```

---

## 🤝 Contributing to SafeExec
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@
"fix:docker": "./scripts/fix-docker-permissions.sh",
"ssl:generate": "./scripts/generate-ssl.sh",
"docker": "echo 'Use yarn docker:dev, yarn docker:test, or yarn docker:prod'",
"docker:dev": "echo '🚀 Starting development environment...' && ENV=development docker compose -f docker-compose.yml up -d",
"docker:dev": "echo '🚀 Starting development environment...' && bash ./scripts/docker-up-auto-port.sh development",
"docker:dev:down": "ENV=development docker compose -f docker-compose.yml down",
"docker:dev:logs": "ENV=development docker compose -f docker-compose.yml logs -f",
"docker:dev:shell": "ENV=development docker compose -f docker-compose.yml exec rce-api sh",
"docker:dev:build": "echo '🏗️ Building development images...' && DOCKER_HOST= ENV=development docker compose -f docker-compose.yml build",
"docker:test": "DOCKER_HOST= ENV=test docker compose up -d",
"docker:test": "DOCKER_HOST= bash ./scripts/docker-up-auto-port.sh test",
"docker:test:down": "ENV=test docker compose down",
"docker:test:run": "ENV=test docker compose run --rm rce-api yarn test",
"docker:test:coverage": "ENV=test docker compose run --rm rce-api yarn test:coverage",
"docker:test:integration": "ENV=test docker compose run --rm rce-api yarn test:integration",
"docker:test:build": "ENV=test docker compose build",
"docker:prod": "DOCKER_HOST= ENV=production docker compose up -d",
"docker:prod": "DOCKER_HOST= bash ./scripts/docker-up-auto-port.sh production",
"docker:prod:down": "ENV=production docker compose down",
"docker:prod:logs": "ENV=production docker compose logs -f",
"docker:prod:shell": "ENV=production docker compose exec rce-api sh",
Expand All @@ -77,7 +77,7 @@
"docker:seed:dev": "ENV=development docker compose exec rce-api yarn seed",
"docker:seed:test": "ENV=test docker compose run --rm rce-api yarn seed",
"docker:seed:prod": "ENV=production docker compose exec rce-api yarn seed",
"docker:restart": "echo '🔄 Restarting development environment...' && docker compose -f docker-compose.yml down && ENV=development docker compose -f docker-compose.yml up -d",
"docker:restart": "echo '🔄 Restarting development environment...' && docker compose -f docker-compose.yml down && yarn docker:dev",
"docker:manage": "echo 'Use yarn docker:dev, yarn docker:test, or yarn docker:prod'",
"setup": "echo '📦 Installing dependencies and building executors...' && yarn install && yarn fix:docker && yarn build:executors",
"setup:dev": "echo '🚀 Complete development setup...' && yarn setup && yarn docker:setup:dev && yarn docker:seed:dev",
Expand Down
130 changes: 130 additions & 0 deletions scripts/docker-up-auto-port.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/bin/bash

# Start docker compose with automatic host port selection.
# If a preferred port is busy, the script increments until a free one is found.

set -euo pipefail

ENVIRONMENT="${1:-development}"
shift || true

COMPOSE_FILE="docker-compose.yml"
MAX_SCAN="${MAX_PORT_SCAN_ATTEMPTS:-200}"

# Keep selected ports unique within this run.
declare -A SELECTED_PORTS

print_info() {
echo "[PORT] $1"
}

print_warn() {
echo "[PORT][WARN] $1"
}

is_numeric_port() {
[[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]
}

is_port_in_use() {
local port="$1"

if command -v ss >/dev/null 2>&1; then
ss -ltnH "( sport = :${port} )" 2>/dev/null | grep -q .
return $?
fi

if command -v lsof >/dev/null 2>&1; then
lsof -iTCP:"${port}" -sTCP:LISTEN -n -P >/dev/null 2>&1
return $?
fi

if command -v netstat >/dev/null 2>&1; then
netstat -ltn 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1
return $?
fi

print_warn "No port-check tool found (ss/lsof/netstat). Assuming port ${port} is free."
return 1
}

is_port_already_selected() {
local port="$1"
[[ -n "${SELECTED_PORTS[$port]:-}" ]]
}

find_next_free_port() {
local start_port="$1"
local label="$2"
local attempt=0
local port="$start_port"

while [ "$attempt" -lt "$MAX_SCAN" ]; do
if ! is_port_in_use "$port" && ! is_port_already_selected "$port"; then
SELECTED_PORTS["$port"]="$label"
echo "$port"
return 0
fi

port=$((port + 1))
if [ "$port" -gt 65535 ]; then
break
fi
attempt=$((attempt + 1))
done

echo ""
return 1
}

resolve_port_var() {
local var_name="$1"
local default_port="$2"
local label="$3"

local requested_port="${!var_name:-$default_port}"

if ! is_numeric_port "$requested_port"; then
print_warn "${var_name} has invalid value '${requested_port}', using default ${default_port}."
requested_port="$default_port"
fi

local resolved_port
resolved_port="$(find_next_free_port "$requested_port" "$label")"

if [ -z "$resolved_port" ]; then
echo "[PORT][ERROR] Could not find free port for ${var_name} after ${MAX_SCAN} attempts from ${requested_port}." >&2
exit 1
fi

if [ "$resolved_port" != "$requested_port" ]; then
print_warn "${label}: ${requested_port} is busy, using ${resolved_port}."
else
print_info "${label}: using ${resolved_port}."
fi

export "${var_name}=${resolved_port}"
}

print_info "Resolving host ports for ENV=${ENVIRONMENT}..."

resolve_port_var "MONGO_PORT" "27017" "MongoDB"
resolve_port_var "REDIS_PORT" "6379" "Redis"
resolve_port_var "API_PORT" "5000" "API"
resolve_port_var "DEBUG_PORT" "9229" "Node Debug"
resolve_port_var "NGINX_HTTP_PORT" "80" "Nginx HTTP"
resolve_port_var "NGINX_HTTPS_PORT" "443" "Nginx HTTPS"

export ENV="${ENVIRONMENT}"

print_info "Starting docker compose with resolved ports..."
docker compose -f "${COMPOSE_FILE}" up -d "$@"

print_info "Docker compose started."
print_info "Resolved host ports:"
print_info "- MongoDB: ${MONGO_PORT}"
print_info "- Redis: ${REDIS_PORT}"
print_info "- API: ${API_PORT}"
print_info "- Debug: ${DEBUG_PORT}"
print_info "- Nginx HTTP: ${NGINX_HTTP_PORT}"
print_info "- Nginx HTTPS: ${NGINX_HTTPS_PORT}"