Skip to content

Commit 6d944d0

Browse files
Merge pull request #6 from vikashkrdeveloper/feat/auto-port-increment
feat: implement automatic host port selection for Docker environment
2 parents 368db7c + 7b31147 commit 6d944d0

File tree

3 files changed

+145
-4
lines changed

3 files changed

+145
-4
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ yarn setup:dev
9595
# - Install all dependencies
9696
# - Build Docker executor containers
9797
# - Start development environment with Docker Compose
98+
# - Auto-shift host ports if default ports are busy (e.g. 27017 -> 27018)
9899
# - Mount source code for hot-reload (changes auto-reload containers)
99100
# - Seed database with sample data
100101
# - Start both API server and worker with live reloading
@@ -255,6 +256,16 @@ yarn docker:clean:all # Complete Docker cleanup
255256
yarn build:executors # Rebuild executor images
256257
```
257258

259+
**Port Already In Use (`address already in use`)**:
260+
261+
```bash
262+
# Retry startup; SafeExec now auto-selects next free host ports
263+
yarn docker:dev
264+
265+
# Example: if 27017 is busy, MongoDB may use 27018
266+
# The selected ports are printed in startup logs
267+
```
268+
258269
---
259270

260271
## 🤝 Contributing to SafeExec

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,18 @@
4848
"fix:docker": "./scripts/fix-docker-permissions.sh",
4949
"ssl:generate": "./scripts/generate-ssl.sh",
5050
"docker": "echo 'Use yarn docker:dev, yarn docker:test, or yarn docker:prod'",
51-
"docker:dev": "echo '🚀 Starting development environment...' && ENV=development docker compose -f docker-compose.yml up -d",
51+
"docker:dev": "echo '🚀 Starting development environment...' && bash ./scripts/docker-up-auto-port.sh development",
5252
"docker:dev:down": "ENV=development docker compose -f docker-compose.yml down",
5353
"docker:dev:logs": "ENV=development docker compose -f docker-compose.yml logs -f",
5454
"docker:dev:shell": "ENV=development docker compose -f docker-compose.yml exec rce-api sh",
5555
"docker:dev:build": "echo '🏗️ Building development images...' && DOCKER_HOST= ENV=development docker compose -f docker-compose.yml build",
56-
"docker:test": "DOCKER_HOST= ENV=test docker compose up -d",
56+
"docker:test": "DOCKER_HOST= bash ./scripts/docker-up-auto-port.sh test",
5757
"docker:test:down": "ENV=test docker compose down",
5858
"docker:test:run": "ENV=test docker compose run --rm rce-api yarn test",
5959
"docker:test:coverage": "ENV=test docker compose run --rm rce-api yarn test:coverage",
6060
"docker:test:integration": "ENV=test docker compose run --rm rce-api yarn test:integration",
6161
"docker:test:build": "ENV=test docker compose build",
62-
"docker:prod": "DOCKER_HOST= ENV=production docker compose up -d",
62+
"docker:prod": "DOCKER_HOST= bash ./scripts/docker-up-auto-port.sh production",
6363
"docker:prod:down": "ENV=production docker compose down",
6464
"docker:prod:logs": "ENV=production docker compose logs -f",
6565
"docker:prod:shell": "ENV=production docker compose exec rce-api sh",
@@ -77,7 +77,7 @@
7777
"docker:seed:dev": "ENV=development docker compose exec rce-api yarn seed",
7878
"docker:seed:test": "ENV=test docker compose run --rm rce-api yarn seed",
7979
"docker:seed:prod": "ENV=production docker compose exec rce-api yarn seed",
80-
"docker:restart": "echo '🔄 Restarting development environment...' && docker compose -f docker-compose.yml down && ENV=development docker compose -f docker-compose.yml up -d",
80+
"docker:restart": "echo '🔄 Restarting development environment...' && docker compose -f docker-compose.yml down && yarn docker:dev",
8181
"docker:manage": "echo 'Use yarn docker:dev, yarn docker:test, or yarn docker:prod'",
8282
"setup": "echo '📦 Installing dependencies and building executors...' && yarn install && yarn fix:docker && yarn build:executors",
8383
"setup:dev": "echo '🚀 Complete development setup...' && yarn setup && yarn docker:setup:dev && yarn docker:seed:dev",

scripts/docker-up-auto-port.sh

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/bin/bash
2+
3+
# Start docker compose with automatic host port selection.
4+
# If a preferred port is busy, the script increments until a free one is found.
5+
6+
set -euo pipefail
7+
8+
ENVIRONMENT="${1:-development}"
9+
shift || true
10+
11+
COMPOSE_FILE="docker-compose.yml"
12+
MAX_SCAN="${MAX_PORT_SCAN_ATTEMPTS:-200}"
13+
14+
# Keep selected ports unique within this run.
15+
declare -A SELECTED_PORTS
16+
17+
print_info() {
18+
echo "[PORT] $1"
19+
}
20+
21+
print_warn() {
22+
echo "[PORT][WARN] $1"
23+
}
24+
25+
is_numeric_port() {
26+
[[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]
27+
}
28+
29+
is_port_in_use() {
30+
local port="$1"
31+
32+
if command -v ss >/dev/null 2>&1; then
33+
ss -ltnH "( sport = :${port} )" 2>/dev/null | grep -q .
34+
return $?
35+
fi
36+
37+
if command -v lsof >/dev/null 2>&1; then
38+
lsof -iTCP:"${port}" -sTCP:LISTEN -n -P >/dev/null 2>&1
39+
return $?
40+
fi
41+
42+
if command -v netstat >/dev/null 2>&1; then
43+
netstat -ltn 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1
44+
return $?
45+
fi
46+
47+
print_warn "No port-check tool found (ss/lsof/netstat). Assuming port ${port} is free."
48+
return 1
49+
}
50+
51+
is_port_already_selected() {
52+
local port="$1"
53+
[[ -n "${SELECTED_PORTS[$port]:-}" ]]
54+
}
55+
56+
find_next_free_port() {
57+
local start_port="$1"
58+
local label="$2"
59+
local attempt=0
60+
local port="$start_port"
61+
62+
while [ "$attempt" -lt "$MAX_SCAN" ]; do
63+
if ! is_port_in_use "$port" && ! is_port_already_selected "$port"; then
64+
SELECTED_PORTS["$port"]="$label"
65+
echo "$port"
66+
return 0
67+
fi
68+
69+
port=$((port + 1))
70+
if [ "$port" -gt 65535 ]; then
71+
break
72+
fi
73+
attempt=$((attempt + 1))
74+
done
75+
76+
echo ""
77+
return 1
78+
}
79+
80+
resolve_port_var() {
81+
local var_name="$1"
82+
local default_port="$2"
83+
local label="$3"
84+
85+
local requested_port="${!var_name:-$default_port}"
86+
87+
if ! is_numeric_port "$requested_port"; then
88+
print_warn "${var_name} has invalid value '${requested_port}', using default ${default_port}."
89+
requested_port="$default_port"
90+
fi
91+
92+
local resolved_port
93+
resolved_port="$(find_next_free_port "$requested_port" "$label")"
94+
95+
if [ -z "$resolved_port" ]; then
96+
echo "[PORT][ERROR] Could not find free port for ${var_name} after ${MAX_SCAN} attempts from ${requested_port}." >&2
97+
exit 1
98+
fi
99+
100+
if [ "$resolved_port" != "$requested_port" ]; then
101+
print_warn "${label}: ${requested_port} is busy, using ${resolved_port}."
102+
else
103+
print_info "${label}: using ${resolved_port}."
104+
fi
105+
106+
export "${var_name}=${resolved_port}"
107+
}
108+
109+
print_info "Resolving host ports for ENV=${ENVIRONMENT}..."
110+
111+
resolve_port_var "MONGO_PORT" "27017" "MongoDB"
112+
resolve_port_var "REDIS_PORT" "6379" "Redis"
113+
resolve_port_var "API_PORT" "5000" "API"
114+
resolve_port_var "DEBUG_PORT" "9229" "Node Debug"
115+
resolve_port_var "NGINX_HTTP_PORT" "80" "Nginx HTTP"
116+
resolve_port_var "NGINX_HTTPS_PORT" "443" "Nginx HTTPS"
117+
118+
export ENV="${ENVIRONMENT}"
119+
120+
print_info "Starting docker compose with resolved ports..."
121+
docker compose -f "${COMPOSE_FILE}" up -d "$@"
122+
123+
print_info "Docker compose started."
124+
print_info "Resolved host ports:"
125+
print_info "- MongoDB: ${MONGO_PORT}"
126+
print_info "- Redis: ${REDIS_PORT}"
127+
print_info "- API: ${API_PORT}"
128+
print_info "- Debug: ${DEBUG_PORT}"
129+
print_info "- Nginx HTTP: ${NGINX_HTTP_PORT}"
130+
print_info "- Nginx HTTPS: ${NGINX_HTTPS_PORT}"

0 commit comments

Comments
 (0)