dockermind 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dockermind/__init__.py +5 -0
- dockermind/analyzer.py +285 -0
- dockermind/cli.py +288 -0
- dockermind/compose_generator.py +43 -0
- dockermind/dockerfile_generator.py +389 -0
- dockermind/doctor.py +88 -0
- dockermind/runner.py +139 -0
- dockermind/utils.py +141 -0
- dockermind-0.1.2.dist-info/METADATA +119 -0
- dockermind-0.1.2.dist-info/RECORD +13 -0
- dockermind-0.1.2.dist-info/WHEEL +5 -0
- dockermind-0.1.2.dist-info/entry_points.txt +2 -0
- dockermind-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dockerfile generator -- produces optimised Dockerfile content from an AnalysisResult.
|
|
3
|
+
|
|
4
|
+
Rules (v1):
|
|
5
|
+
- Node with build script -> multi-stage (builder + runtime)
|
|
6
|
+
- Node without build -> single-stage
|
|
7
|
+
- Python -> single-stage with pip install
|
|
8
|
+
- Never use Alpine images -- always -slim (Debian bookworm)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dockermind.analyzer import AnalysisResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_dockerfile(analysis: AnalysisResult) -> str:
|
|
17
|
+
"""Return the full Dockerfile content as a string."""
|
|
18
|
+
if analysis.language == "node":
|
|
19
|
+
return _generate_node(analysis)
|
|
20
|
+
elif analysis.language == "python":
|
|
21
|
+
return _generate_python(analysis)
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError(f"Unsupported language: {analysis.language}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generate_dockerignore(analysis: AnalysisResult) -> str:
|
|
27
|
+
"""Return .dockerignore content appropriate for the project."""
|
|
28
|
+
if analysis.language == "node":
|
|
29
|
+
return _node_dockerignore()
|
|
30
|
+
elif analysis.language == "python":
|
|
31
|
+
return _python_dockerignore()
|
|
32
|
+
return _generic_dockerignore()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Node.js Dockerfile generation
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _generate_node(analysis: AnalysisResult) -> str:
|
|
41
|
+
"""Generate a Node.js Dockerfile.
|
|
42
|
+
|
|
43
|
+
Multi-stage when a build script exists; single-stage otherwise.
|
|
44
|
+
"""
|
|
45
|
+
version = analysis.node_version
|
|
46
|
+
base_image = f"node:{version}-slim"
|
|
47
|
+
|
|
48
|
+
# Determine install command based on lock file
|
|
49
|
+
install_cmd = _node_install_cmd(analysis)
|
|
50
|
+
install_cmd_prod = _node_install_cmd(analysis, production=True)
|
|
51
|
+
|
|
52
|
+
# Determine the CMD
|
|
53
|
+
start_cmd = _node_start_cmd(analysis)
|
|
54
|
+
|
|
55
|
+
port = analysis.port or 3000
|
|
56
|
+
|
|
57
|
+
if analysis.has_build_script:
|
|
58
|
+
return _node_multistage(
|
|
59
|
+
base_image=base_image,
|
|
60
|
+
install_cmd=install_cmd,
|
|
61
|
+
install_cmd_prod=install_cmd_prod,
|
|
62
|
+
build_script=analysis.build_script or "npm run build",
|
|
63
|
+
start_cmd=start_cmd,
|
|
64
|
+
port=port,
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
return _node_singlestage(
|
|
68
|
+
base_image=base_image,
|
|
69
|
+
install_cmd=install_cmd_prod,
|
|
70
|
+
start_cmd=start_cmd,
|
|
71
|
+
port=port,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _node_multistage(
|
|
76
|
+
*,
|
|
77
|
+
base_image: str,
|
|
78
|
+
install_cmd: str,
|
|
79
|
+
install_cmd_prod: str,
|
|
80
|
+
build_script: str,
|
|
81
|
+
start_cmd: str,
|
|
82
|
+
port: int,
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Multi-stage Node.js Dockerfile (build + runtime)."""
|
|
85
|
+
|
|
86
|
+
# Determine run build command
|
|
87
|
+
if "pnpm" in install_cmd:
|
|
88
|
+
run_build = "pnpm run build"
|
|
89
|
+
elif "yarn" in install_cmd:
|
|
90
|
+
run_build = "yarn build"
|
|
91
|
+
else:
|
|
92
|
+
run_build = "npm run build"
|
|
93
|
+
|
|
94
|
+
lines = [
|
|
95
|
+
f"# ---- Stage 1: Build ----",
|
|
96
|
+
f"FROM {base_image} AS builder",
|
|
97
|
+
"",
|
|
98
|
+
"WORKDIR /app",
|
|
99
|
+
"",
|
|
100
|
+
"# Copy dependency manifests first for better layer caching",
|
|
101
|
+
"COPY package*.json ./",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Copy lock files based on what exists
|
|
105
|
+
if "pnpm" in install_cmd:
|
|
106
|
+
lines.append("COPY pnpm-lock.yaml ./")
|
|
107
|
+
elif "yarn" in install_cmd:
|
|
108
|
+
lines.append("COPY yarn.lock ./")
|
|
109
|
+
|
|
110
|
+
lines += [
|
|
111
|
+
"",
|
|
112
|
+
f"RUN {install_cmd}",
|
|
113
|
+
"",
|
|
114
|
+
"# Copy source code and build",
|
|
115
|
+
"COPY . .",
|
|
116
|
+
f"RUN {run_build}",
|
|
117
|
+
"",
|
|
118
|
+
f"# ---- Stage 2: Production ----",
|
|
119
|
+
f"FROM {base_image}",
|
|
120
|
+
"",
|
|
121
|
+
"WORKDIR /app",
|
|
122
|
+
"",
|
|
123
|
+
"# Copy dependency manifests",
|
|
124
|
+
"COPY package*.json ./",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
if "pnpm" in install_cmd_prod:
|
|
128
|
+
lines.append("COPY pnpm-lock.yaml ./")
|
|
129
|
+
elif "yarn" in install_cmd_prod:
|
|
130
|
+
lines.append("COPY yarn.lock ./")
|
|
131
|
+
|
|
132
|
+
lines += [
|
|
133
|
+
"",
|
|
134
|
+
f"# Install production dependencies only",
|
|
135
|
+
f"RUN {install_cmd_prod}",
|
|
136
|
+
"",
|
|
137
|
+
"# Copy built output from builder stage",
|
|
138
|
+
"COPY --from=builder /app/dist ./dist",
|
|
139
|
+
"",
|
|
140
|
+
f"EXPOSE {port}",
|
|
141
|
+
"",
|
|
142
|
+
f'CMD {start_cmd}',
|
|
143
|
+
"",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
return "\n".join(lines)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _node_singlestage(
|
|
150
|
+
*,
|
|
151
|
+
base_image: str,
|
|
152
|
+
install_cmd: str,
|
|
153
|
+
start_cmd: str,
|
|
154
|
+
port: int,
|
|
155
|
+
) -> str:
|
|
156
|
+
"""Single-stage Node.js Dockerfile (no build step)."""
|
|
157
|
+
lines = [
|
|
158
|
+
f"FROM {base_image}",
|
|
159
|
+
"",
|
|
160
|
+
"WORKDIR /app",
|
|
161
|
+
"",
|
|
162
|
+
"# Copy dependency manifests first for better layer caching",
|
|
163
|
+
"COPY package*.json ./",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
if "pnpm" in install_cmd:
|
|
167
|
+
lines.append("COPY pnpm-lock.yaml ./")
|
|
168
|
+
elif "yarn" in install_cmd:
|
|
169
|
+
lines.append("COPY yarn.lock ./")
|
|
170
|
+
|
|
171
|
+
lines += [
|
|
172
|
+
"",
|
|
173
|
+
f"RUN {install_cmd}",
|
|
174
|
+
"",
|
|
175
|
+
"# Copy application source",
|
|
176
|
+
"COPY . .",
|
|
177
|
+
"",
|
|
178
|
+
f"EXPOSE {port}",
|
|
179
|
+
"",
|
|
180
|
+
f'CMD {start_cmd}',
|
|
181
|
+
"",
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
return "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _node_install_cmd(analysis: AnalysisResult, *, production: bool = False) -> str:
|
|
188
|
+
"""Return the appropriate npm/yarn/pnpm install command."""
|
|
189
|
+
if analysis.has_pnpm_lock:
|
|
190
|
+
# pnpm requires corepack or global install; keep it simple
|
|
191
|
+
base = "corepack enable && pnpm install"
|
|
192
|
+
if production:
|
|
193
|
+
return base + " --prod"
|
|
194
|
+
return base + " --frozen-lockfile"
|
|
195
|
+
|
|
196
|
+
if analysis.has_yarn_lock:
|
|
197
|
+
if production:
|
|
198
|
+
return "yarn install --production --frozen-lockfile"
|
|
199
|
+
return "yarn install --frozen-lockfile"
|
|
200
|
+
|
|
201
|
+
# Default: npm
|
|
202
|
+
if production:
|
|
203
|
+
return "npm ci --omit=dev"
|
|
204
|
+
return "npm ci"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _node_start_cmd(analysis: AnalysisResult) -> str:
|
|
208
|
+
"""Determine the CMD instruction for a Node project."""
|
|
209
|
+
if analysis.start_script:
|
|
210
|
+
script = analysis.start_script.strip()
|
|
211
|
+
|
|
212
|
+
# If the start script begins with "node", extract the entry file safely.
|
|
213
|
+
if script.startswith("node "):
|
|
214
|
+
try:
|
|
215
|
+
# Use shlex-style split to handle paths with spaces / flags.
|
|
216
|
+
parts = script.split()
|
|
217
|
+
# parts[0] == "node", parts[1:] are args.
|
|
218
|
+
if len(parts) >= 2:
|
|
219
|
+
entry_file = parts[1].strip()
|
|
220
|
+
if entry_file:
|
|
221
|
+
return f'["node", "{entry_file}"]'
|
|
222
|
+
except (IndexError, ValueError):
|
|
223
|
+
pass # fall through to package-manager start below
|
|
224
|
+
|
|
225
|
+
# Otherwise wrap in npm/yarn/pnpm start
|
|
226
|
+
if analysis.has_pnpm_lock:
|
|
227
|
+
return '["pnpm", "start"]'
|
|
228
|
+
if analysis.has_yarn_lock:
|
|
229
|
+
return '["yarn", "start"]'
|
|
230
|
+
return '["npm", "start"]'
|
|
231
|
+
|
|
232
|
+
# No start script -- try common entry files
|
|
233
|
+
return '["node", "index.js"]'
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Python Dockerfile generation
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _infer_system_packages(analysis: AnalysisResult) -> list[str]:
|
|
242
|
+
"""Return a list of apt packages required by detected Python dependencies."""
|
|
243
|
+
mapping: dict[str, str] = {
|
|
244
|
+
"psycopg2": "libpq-dev",
|
|
245
|
+
"pillow": "libjpeg-dev",
|
|
246
|
+
"lxml": "libxml2-dev",
|
|
247
|
+
"numpy": "build-essential",
|
|
248
|
+
}
|
|
249
|
+
packages: list[str] = []
|
|
250
|
+
deps = analysis.python_deps
|
|
251
|
+
for dep_substring, apt_pkg in mapping.items():
|
|
252
|
+
if any(dep_substring in d for d in deps) and apt_pkg not in packages:
|
|
253
|
+
packages.append(apt_pkg)
|
|
254
|
+
return sorted(packages)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _generate_python(analysis: AnalysisResult) -> str:
|
|
258
|
+
"""Generate a Python Dockerfile (always single-stage)."""
|
|
259
|
+
version = analysis.python_version
|
|
260
|
+
base_image = f"python:{version}-slim"
|
|
261
|
+
port = analysis.port or 8000
|
|
262
|
+
|
|
263
|
+
cmd = _python_cmd(analysis, port)
|
|
264
|
+
system_packages = _infer_system_packages(analysis)
|
|
265
|
+
|
|
266
|
+
lines = [
|
|
267
|
+
f"FROM {base_image}",
|
|
268
|
+
"",
|
|
269
|
+
"# Prevent Python from writing .pyc files and enable unbuffered output",
|
|
270
|
+
"ENV PYTHONDONTWRITEBYTECODE=1",
|
|
271
|
+
"ENV PYTHONUNBUFFERED=1",
|
|
272
|
+
"",
|
|
273
|
+
"WORKDIR /app",
|
|
274
|
+
"",
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
# Install system-level native dependencies if needed
|
|
278
|
+
if system_packages:
|
|
279
|
+
pkg_list = " ".join(system_packages)
|
|
280
|
+
lines += [
|
|
281
|
+
"# Install native build dependencies",
|
|
282
|
+
"RUN apt-get update && \\",
|
|
283
|
+
f" apt-get install -y {pkg_list} && \\",
|
|
284
|
+
" rm -rf /var/lib/apt/lists/*",
|
|
285
|
+
"",
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
lines += [
|
|
289
|
+
"# Install dependencies first for better layer caching",
|
|
290
|
+
"COPY requirements.txt ./",
|
|
291
|
+
"RUN pip install --no-cache-dir -r requirements.txt",
|
|
292
|
+
"",
|
|
293
|
+
"# Copy application source",
|
|
294
|
+
"COPY . .",
|
|
295
|
+
"",
|
|
296
|
+
f"EXPOSE {port}",
|
|
297
|
+
"",
|
|
298
|
+
f'CMD {cmd}',
|
|
299
|
+
"",
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
return "\n".join(lines)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _python_cmd(analysis: AnalysisResult, port: int) -> str:
|
|
306
|
+
"""Determine the CMD instruction for a Python project."""
|
|
307
|
+
entry = analysis.python_entry_module or "main:app"
|
|
308
|
+
|
|
309
|
+
if analysis.uses_uvicorn or analysis.framework == "fastapi":
|
|
310
|
+
return f'["uvicorn", "{entry}", "--host", "0.0.0.0", "--port", "{port}"]'
|
|
311
|
+
|
|
312
|
+
if analysis.framework == "flask":
|
|
313
|
+
# For Flask, use flask run with proper host/port binding
|
|
314
|
+
module = entry.split(":")[0] if ":" in entry else "app"
|
|
315
|
+
return (
|
|
316
|
+
f'["flask", "run", "--host", "0.0.0.0", "--port", "{port}"]'
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Generic fallback
|
|
320
|
+
return f'["python", "-m", "{entry.split(":")[0] if ":" in entry else entry}"]'
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
# .dockerignore content
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _node_dockerignore() -> str:
|
|
329
|
+
return """\
|
|
330
|
+
node_modules
|
|
331
|
+
npm-debug.log*
|
|
332
|
+
.git
|
|
333
|
+
.gitignore
|
|
334
|
+
.dockerignore
|
|
335
|
+
Dockerfile
|
|
336
|
+
docker-compose*.yml
|
|
337
|
+
.env
|
|
338
|
+
.env.*
|
|
339
|
+
.vscode
|
|
340
|
+
.idea
|
|
341
|
+
coverage
|
|
342
|
+
.nyc_output
|
|
343
|
+
dist
|
|
344
|
+
build
|
|
345
|
+
*.md
|
|
346
|
+
LICENSE
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _python_dockerignore() -> str:
|
|
351
|
+
return """\
|
|
352
|
+
__pycache__
|
|
353
|
+
*.pyc
|
|
354
|
+
*.pyo
|
|
355
|
+
.git
|
|
356
|
+
.gitignore
|
|
357
|
+
.dockerignore
|
|
358
|
+
Dockerfile
|
|
359
|
+
docker-compose*.yml
|
|
360
|
+
.env
|
|
361
|
+
.env.*
|
|
362
|
+
.vscode
|
|
363
|
+
.idea
|
|
364
|
+
.venv
|
|
365
|
+
venv
|
|
366
|
+
.tox
|
|
367
|
+
.pytest_cache
|
|
368
|
+
*.egg-info
|
|
369
|
+
dist
|
|
370
|
+
build
|
|
371
|
+
*.md
|
|
372
|
+
LICENSE
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _generic_dockerignore() -> str:
|
|
377
|
+
return """\
|
|
378
|
+
.git
|
|
379
|
+
.gitignore
|
|
380
|
+
.dockerignore
|
|
381
|
+
Dockerfile
|
|
382
|
+
docker-compose*.yml
|
|
383
|
+
.env
|
|
384
|
+
.env.*
|
|
385
|
+
.vscode
|
|
386
|
+
.idea
|
|
387
|
+
*.md
|
|
388
|
+
LICENSE
|
|
389
|
+
"""
|
dockermind/doctor.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker doctor -- checks that Docker is installed and running.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from dockermind.utils import command_available, run_command, green, red, yellow, dim
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_docker_installed() -> bool:
|
|
13
|
+
"""Return True if the ``docker`` binary is on the PATH."""
|
|
14
|
+
return command_available("docker")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_docker_running() -> bool:
|
|
18
|
+
"""Return True if the Docker daemon is responsive."""
|
|
19
|
+
try:
|
|
20
|
+
run_command(["docker", "info"], capture=True)
|
|
21
|
+
return True
|
|
22
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_doctor() -> bool:
|
|
27
|
+
"""Run all health checks and print results. Return True if everything passes."""
|
|
28
|
+
all_ok = True
|
|
29
|
+
|
|
30
|
+
print()
|
|
31
|
+
print(" Docker Environment Check")
|
|
32
|
+
print(" " + "-" * 40)
|
|
33
|
+
|
|
34
|
+
# Check 1: Docker installed
|
|
35
|
+
if check_docker_installed():
|
|
36
|
+
print(f" {green('PASS')} Docker CLI is installed")
|
|
37
|
+
else:
|
|
38
|
+
print(f" {red('FAIL')} Docker CLI is not installed")
|
|
39
|
+
print(f" {dim('Install Docker: https://docs.docker.com/get-docker/')}")
|
|
40
|
+
all_ok = False
|
|
41
|
+
|
|
42
|
+
# Check 2: Docker daemon running
|
|
43
|
+
if all_ok:
|
|
44
|
+
if check_docker_running():
|
|
45
|
+
print(f" {green('PASS')} Docker daemon is running")
|
|
46
|
+
|
|
47
|
+
# Bonus: show Docker version
|
|
48
|
+
try:
|
|
49
|
+
result = run_command(["docker", "version", "--format", "{{.Server.Version}}"])
|
|
50
|
+
version = result.stdout.strip()
|
|
51
|
+
print(f" {dim(f'Server version: {version}')}")
|
|
52
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
53
|
+
pass
|
|
54
|
+
else:
|
|
55
|
+
print(f" {red('FAIL')} Docker daemon is not running")
|
|
56
|
+
print(f" {dim('Start Docker Desktop or run: sudo systemctl start docker')}")
|
|
57
|
+
all_ok = False
|
|
58
|
+
|
|
59
|
+
# Check 3: Docker Compose available
|
|
60
|
+
compose_available = False
|
|
61
|
+
try:
|
|
62
|
+
run_command(["docker", "compose", "version"], capture=True)
|
|
63
|
+
compose_available = True
|
|
64
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if not compose_available:
|
|
68
|
+
# Try legacy docker-compose
|
|
69
|
+
try:
|
|
70
|
+
run_command(["docker-compose", "version"], capture=True)
|
|
71
|
+
compose_available = True
|
|
72
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
if compose_available:
|
|
76
|
+
print(f" {green('PASS')} Docker Compose is available")
|
|
77
|
+
else:
|
|
78
|
+
print(f" {yellow('WARN')} Docker Compose not found (optional)")
|
|
79
|
+
|
|
80
|
+
print()
|
|
81
|
+
|
|
82
|
+
if all_ok:
|
|
83
|
+
print(f" {green('All checks passed. Ready to build.')}")
|
|
84
|
+
else:
|
|
85
|
+
print(f" {red('Some checks failed. Fix the issues above.')}")
|
|
86
|
+
|
|
87
|
+
print()
|
|
88
|
+
return all_ok
|
dockermind/runner.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker runner -- wraps ``docker build`` and ``docker run`` with clean error handling.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from dockermind.utils import green, red, yellow, dim, bold
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _check_port_available(port: int) -> bool:
|
|
15
|
+
"""Return True if *port* is free on localhost; False if already bound."""
|
|
16
|
+
try:
|
|
17
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
18
|
+
s.bind(("127.0.0.1", port))
|
|
19
|
+
return True
|
|
20
|
+
except OSError:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def docker_build(
|
|
25
|
+
project_path: str,
|
|
26
|
+
*,
|
|
27
|
+
tag: str = "dockermind-app",
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""Run ``docker build`` and stream output. Return True on success."""
|
|
30
|
+
print()
|
|
31
|
+
print(f" {bold('Building image:')} {tag}")
|
|
32
|
+
print(f" {dim(f'Context: {project_path}')}")
|
|
33
|
+
print()
|
|
34
|
+
|
|
35
|
+
cmd = [
|
|
36
|
+
"docker", "build",
|
|
37
|
+
"-t", tag,
|
|
38
|
+
project_path,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Stream build output directly to terminal
|
|
43
|
+
process = subprocess.Popen(
|
|
44
|
+
cmd,
|
|
45
|
+
stdout=sys.stdout,
|
|
46
|
+
stderr=sys.stderr,
|
|
47
|
+
)
|
|
48
|
+
exit_code = process.wait()
|
|
49
|
+
|
|
50
|
+
print()
|
|
51
|
+
if exit_code == 0:
|
|
52
|
+
print(f" {green(f'Image built successfully: {tag}')}")
|
|
53
|
+
return True
|
|
54
|
+
else:
|
|
55
|
+
print(f" {red(f'Build failed with exit code {exit_code}')}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
print(f" {red('Docker is not installed or not on PATH.')}")
|
|
60
|
+
print(f" {dim('Run `dockermind doctor` to diagnose.')}")
|
|
61
|
+
return False
|
|
62
|
+
except OSError as e:
|
|
63
|
+
print(f" {red(f'Failed to run docker build: {e}')}")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def docker_run(
|
|
68
|
+
*,
|
|
69
|
+
tag: str = "dockermind-app",
|
|
70
|
+
port: int = 3000,
|
|
71
|
+
detach: bool = False,
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""Run ``docker run`` with port mapping. Return True on success."""
|
|
74
|
+
print()
|
|
75
|
+
print(f" {bold('Running container:')} {tag}")
|
|
76
|
+
print(f" {dim(f'Port mapping: {port}:{port}')}")
|
|
77
|
+
|
|
78
|
+
# Lightweight port-in-use warning (non-blocking)
|
|
79
|
+
if not _check_port_available(port):
|
|
80
|
+
print(f" {yellow(f'Warning: Port {port} appears to be in use.')}")
|
|
81
|
+
|
|
82
|
+
print()
|
|
83
|
+
|
|
84
|
+
cmd = [
|
|
85
|
+
"docker", "run",
|
|
86
|
+
"--rm",
|
|
87
|
+
"-p", f"{port}:{port}",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
if detach:
|
|
91
|
+
cmd.append("-d")
|
|
92
|
+
|
|
93
|
+
cmd.append(tag)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if detach:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
cmd,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
)
|
|
102
|
+
if result.returncode == 0:
|
|
103
|
+
container_id = result.stdout.strip()[:12]
|
|
104
|
+
print(f" {green(f'Container started: {container_id}')}")
|
|
105
|
+
print(f" {dim(f'Listening on http://localhost:{port}')}")
|
|
106
|
+
return True
|
|
107
|
+
else:
|
|
108
|
+
print(f" {red(f'Failed to start container.')}")
|
|
109
|
+
if result.stderr:
|
|
110
|
+
print(f" {dim(result.stderr.strip())}")
|
|
111
|
+
return False
|
|
112
|
+
else:
|
|
113
|
+
# Foreground mode -- stream output
|
|
114
|
+
print(f" {dim(f'Listening on http://localhost:{port}')}")
|
|
115
|
+
print(f" {dim('Press Ctrl+C to stop.')}")
|
|
116
|
+
print()
|
|
117
|
+
|
|
118
|
+
process = subprocess.Popen(
|
|
119
|
+
cmd,
|
|
120
|
+
stdout=sys.stdout,
|
|
121
|
+
stderr=sys.stderr,
|
|
122
|
+
)
|
|
123
|
+
try:
|
|
124
|
+
exit_code = process.wait()
|
|
125
|
+
except KeyboardInterrupt:
|
|
126
|
+
print()
|
|
127
|
+
print(f" {yellow('Stopping container...')}")
|
|
128
|
+
process.terminate()
|
|
129
|
+
process.wait(timeout=10)
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
return exit_code == 0
|
|
133
|
+
|
|
134
|
+
except FileNotFoundError:
|
|
135
|
+
print(f" {red('Docker is not installed or not on PATH.')}")
|
|
136
|
+
return False
|
|
137
|
+
except OSError as e:
|
|
138
|
+
print(f" {red(f'Failed to run docker run: {e}')}")
|
|
139
|
+
return False
|