Docker 里配代理:容器运行时和 build 阶段的正确写法

一份 docker-compose.yml,改造成让容器 和 构建过程都能走代理,看起来简单,实际上分两层——很多人只加了一层就以为好了。 场景一:容器运行时走代理 只要在 environment 里加 4 个变量(大小写都写一份最保险): services: web: build: context: . dockerfile: Dockerfile environment: http_proxy: http://host.docker.internal:7890 https_proxy: http://host.docker.internal:7890 HTTP_PROXY: http://host.docker.internal:7890 HTTPS_PROXY: http://host.docker.internal:7890 extra_hosts: - "host.docker.internal:host-gateway"host.docker.internal 是宿主机地址(Mac / Windows 原生支持) Linux 需要显式声明 extra_hosts——不然容器里根本不认这个名字宿主机上代理是 Clash / v2ray,默认端口通常 7890。用之前先在宿主机测: curl -x http://127.0.0.1:7890 https://www.google.com场景二:build 阶段也要走代理 environment 只影响运行时。build 阶段 apt install、git clone、pecl 这些走的还是宿主机网络。要让它们也走代理,得从 args 传进去,再在 Dockerfile 里 ARG + ENV。 docker-compose.yml: services: web: build: context: . dockerfile: Dockerfile args: http_proxy: http://host.docker.internal:7890 https_proxy: http://host.docker.internal:7890 extra_hosts: - "host.docker.internal:host-gateway"Dockerfile 顶部: FROM php:7.4-apacheARG http_proxy ARG https_proxyENV http_proxy=$http_proxy ENV https_proxy=$https_proxy ENV HTTP_PROXY=$http_proxy ENV HTTPS_PROXY=$https_proxy# 之后所有 apt / curl / git / composer 都会走代理写在最前面很重要——ENV 只对后续 RUN 生效。 Linux 老坑:host.docker.internal 不通 Linux 上 host.docker.internal 默认解析不了。有两种解法: 推荐:extra_hosts + host-gateway extra_hosts: - "host.docker.internal:host-gateway"这是 Docker 20.10+ 的标准写法,比手动填 IP 稳。 兜底:用 docker0 网桥 IP environment: http_proxy: http://172.17.0.1:7890前提是宿主机代理监听 0.0.0.0 或者 172.17.0.1——只监听 127.0.0.1 是不行的。 构建后清掉代理(可选) 代理只用于 build 阶段,不希望被镜像继承的话: FROM baseARG http_proxy ARG https_proxyENV http_proxy=$http_proxy https_proxy=$https_proxy RUN apt-get update && apt-get install -y curl git# 用完就抹掉 ENV http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY=镜像跑起来后就不会带代理设置了。 一句话总结 environment 只管跑起来之后,build 阶段的代理要靠 args + ARG + ENV。Linux 上别忘 extra_hosts: host-gateway。

Docker Compose 容器内配置 HTTP 代理:environment 变量与 host.docker.internal

Docker 容器不会自动继承宿主机的代理设置,容器内部访问外网需要显式注入代理环境变量。 在 docker-compose.yml 中注入代理 在需要走代理的服务 environment 中添加: services: web: image: nginx:latest environment: http_proxy: http://172.17.0.1:7890 https_proxy: http://172.17.0.1:7890 HTTP_PROXY: http://172.17.0.1:7890 HTTPS_PROXY: http://172.17.0.1:7890 NO_PROXY: localhost,127.0.0.1,172.17.0.0/16同时写大写和小写两种形式,因为不同程序读取的环境变量名不一致(curl/wget 读小写,Java/Python 可能读大写)。 Linux 宿主机代理地址 Linux 下 Docker 默认网桥 IP 是 172.17.0.1,容器内可以用它访问宿主机上运行的代理: # 先在宿主机验证端口可达 curl 172.17.0.1:7890更推荐的写法是用 host-gateway,语义更清晰且不依赖具体网桥 IP: services: web: extra_hosts: - "host.docker.internal:host-gateway" environment: http_proxy: http://host.docker.internal:7890 https_proxy: http://host.docker.internal:7890host.docker.internal 在 macOS/Windows 的 Docker Desktop 中默认可用,Linux 需要通过 extra_hosts 手动映射。 Dockerfile 构建阶段代理 environment 只对运行中的容器生效,docker build 阶段(apt install、pip install、git clone)需要在 Dockerfile 里设置: FROM ubuntu:24.04ENV http_proxy=http://host.docker.internal:7890 ENV https_proxy=http://host.docker.internal:7890RUN apt-get update && apt-get install -y curl或者构建时通过 --build-arg 传入(更灵活,不会写死到镜像层): ARG http_proxy ARG https_proxy RUN apt-get update && apt-get install -y curldocker build \ --build-arg http_proxy=http://172.17.0.1:7890 \ --build-arg https_proxy=http://172.17.0.1:7890 \ -t myimage .常见问题 容器外能上网,容器内不行 → 忘记设置 environment 代理变量。 代理地址用 127.0.0.1 不通 → 容器的 127.0.0.1 指向容器本身而非宿主机,必须用 172.17.0.1 或 host.docker.internal。 https_proxy 写成 https:// → 代理地址的 scheme 必须用 http://,即使是 HTTPS 流量也一样: HTTPS_PROXY=https://... ← 错误 HTTPS_PROXY=http://... ← 正确

OpenCV 识别户型图中的墙线与柱子:HoughLines、轮廓检测与 YOLO

图片/CAD 图纸方案 墙线识别 import cv2 import numpy as npimg = cv2.imread("floorplan.png") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 边缘检测 edges = cv2.Canny(gray, 50, 150, apertureSize=3)# 霍夫直线检测 lines = cv2.HoughLinesP( edges, rho=1, theta=np.pi / 180, threshold=100, minLineLength=50, maxLineGap=10 )if lines is not None: for line in lines: x1, y1, x2, y2 = line[0] cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)minLineLength 控制最短检测长度,maxLineGap 控制允许的断线间距。 圆柱识别 circles = cv2.HoughCircles( gray, cv2.HOUGH_GRADIENT, dp=1, minDist=20, param1=50, param2=30, minRadius=10, maxRadius=100 )if circles is not None: circles = np.uint16(circles[0]) for cx, cy, r in circles: cv2.circle(img, (cx, cy), r, (255, 0, 0), 2)方柱/立柱识别 通过轮廓检测找矩形区域: contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)for cnt in contours: area = cv2.contourArea(cnt) if area < 200: continue # 多边形逼近 epsilon = 0.02 * cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, epsilon, True) # 4 个顶点且面积合理 → 矩形 → 立柱 if len(approx) == 4: x, y, w, h = cv2.boundingRect(approx) aspect = w / h if h > 0 else 0 if 0.5 < aspect < 2.0: # 长宽比合理 cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)3D 点云方案(激光雷达/深度图) 墙面提取(RANSAC 平面分割) import open3d as o3dpcd = o3d.io.read_point_cloud("scan.ply")# RANSAC 平面分割 plane_model, inliers = pcd.segment_plane( distance_threshold=0.01, ransac_n=3, num_iterations=1000 )wall_cloud = pcd.select_by_index(inliers) other_cloud = pcd.select_by_index(inliers, invert=True)重复调用提取多个平面(多面墙体)。 圆柱提取(PCL RANSAC) PCL 提供 SACMODEL_CYLINDER 直接拟合圆柱: pcl::SACSegmentationFromNormals<PointT, NormalT> seg; seg.setModelType(pcl::SACMODEL_CYLINDER); seg.setMethodType(pcl::SAC_RANSAC); seg.setDistanceThreshold(0.05); seg.segment(*inliers, *coefficients);Python 可通过 python-pcl 或 Open3D 的 cylinder_fit 实现。 实时摄像头方案(推荐) 传统几何算法在以下情况容易失效:图纸断线、噪声 光照变化 非标准图纸格式推荐直接用 YOLO 检测/分割模型: from ultralytics import YOLOmodel = YOLO("yolov8n-seg.pt")# 训练自定义数据集(wall / pillar / cylinder 三类) model.train(data="floorplan.yaml", epochs=100, imgsz=640)# 推理 results = model("room.jpg") results[0].show()YOLOv8-seg 直接输出分割 Mask,比 HoughLines 对真实环境更鲁棒。 方案选型建议场景 推荐方案标准黑白 CAD 图 Canny + HoughLinesP平面图圆柱 HoughCircles复杂/真实户型图 轮廓检测 + 面积过滤激光雷达点云 RANSAC 平面/圆柱拟合实时摄像头 YOLOv8-seg 训练定制模型实际项目中通常是"深度学习检测 + 几何后处理"组合使用,单纯依赖传统算法稳定性较差。

OkHttp 发起 HTTP 请求:POST JSON、表单、文件上传与异步调用

Maven 依赖 <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency>GET 请求 OkHttpClient client = new OkHttpClient();Request request = new Request.Builder() .url("https://api.example.com/users/1") .build();try (Response response = client.newCall(request).execute()) { String body = response.body().string(); System.out.println(body); }POST JSON OkHttpClient client = new OkHttpClient();MediaType JSON = MediaType.get("application/json; charset=utf-8"); String json = "{\"username\": \"admin\", \"password\": \"secret\"}";RequestBody body = RequestBody.create(json, JSON);Request request = new Request.Builder() .url("https://api.example.com/login") .post(body) .build();try (Response response = client.newCall(request).execute()) { System.out.println(response.body().string()); }自定义 Header Request request = new Request.Builder() .url("https://api.example.com/data") .header("Authorization", "Bearer your-token") .header("Content-Type", "application/json") .post(body) .build();表单提交(application/x-www-form-urlencoded) RequestBody formBody = new FormBody.Builder() .add("username", "admin") .add("password", "secret") .build();Request request = new Request.Builder() .url("https://api.example.com/login") .post(formBody) .build();文件上传(multipart/form-data) File file = new File("/path/to/file.jpg");RequestBody fileBody = RequestBody.create(file, MediaType.get("image/jpeg"));RequestBody multipart = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", file.getName(), fileBody) .addFormDataPart("description", "头像上传") .build();Request request = new Request.Builder() .url("https://api.example.com/upload") .post(multipart) .build();超时配置 OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build();异步请求(enqueue) client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody body = response.body()) { System.out.println(body.string()); } } });异步回调在 OkHttp 内部线程池中执行,Android 中更新 UI 需要切回主线程。 封装工具类 public class HttpUtil { private static final OkHttpClient CLIENT = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); public static String get(String url) throws IOException { Request request = new Request.Builder().url(url).build(); try (Response response = CLIENT.newCall(request).execute()) { return response.body().string(); } } public static String postJson(String url, String json) throws IOException { RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder().url(url).post(body).build(); try (Response response = CLIENT.newCall(request).execute()) { return response.body().string(); } } }OkHttpClient 实例应该全局复用,避免每次请求创建新实例浪费连接池资源。

Laravel + ClickHouse 统计数据推送飞书卡片

用 Laravel 定时任务对接 ClickHouse 日志,每小时/每天推送应用流量报告到飞书群。 核心架构 ClickHouse (request_logs) → Laravel Artisan Command → 飞书 Webhook → 交互式卡片定时任务两种模式:push:feishu-stats — 每小时统计近 1h / 3h 流量 push:feishu-stats daily — 每天 0 点统计近 24h / 3d / 7dArtisan Command <?phpnamespace App\Console\Commands;use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; use Carbon\Carbon; use App\Services\ClickHouseService;class PushFeishuStats extends Command { protected $signature = 'push:feishu-stats {type=hourly}'; protected $description = '推送应用访问统计数据到飞书群'; protected ClickHouseService $clickHouseService; public function __construct(ClickHouseService $clickHouseService) { parent::__construct(); $this->clickHouseService = $clickHouseService; } public function handle() { $type = $this->argument('type'); try { $type === 'daily' ? $this->handleDailyPush() : $this->handleHourlyPush(); return Command::SUCCESS; } catch (\Throwable $e) { $this->error($e->getMessage()); return Command::FAILURE; } } protected function handleHourlyPush() { $now = Carbon::now(); $oneHourAgo = $now->copy()->subHour()->toDateTimeString(); $threeHoursAgo = $now->copy()->subHours(3)->toDateTimeString(); $sql = " SELECT JSONExtractString(request_params, 'appid') AS appid, COUNTIf(timestamp >= '{$oneHourAgo}') AS count_1h, COUNT(1) AS count_3h FROM request_logs WHERE timestamp >= '{$threeHoursAgo}' GROUP BY appid HAVING count_3h > 10 ORDER BY count_3h DESC LIMIT 20 "; $rows = $this->query($sql); $this->sendToFeishu('📊 每小时应用流量监控', $rows, 'hourly'); } protected function handleDailyPush() { $now = Carbon::now(); $oneDayAgo = $now->copy()->subDay()->toDateTimeString(); $threeDaysAgo = $now->copy()->subDays(3)->toDateTimeString(); $sevenDaysAgo = $now->copy()->subDays(7)->toDateTimeString(); $sql = " SELECT JSONExtractString(request_params, 'appid') AS appid, COUNTIf(timestamp >= '{$oneDayAgo}') AS count_24h, COUNTIf(timestamp >= '{$threeDaysAgo}') AS count_3d, COUNT(1) AS count_7d FROM request_logs WHERE timestamp >= '{$sevenDaysAgo}' GROUP BY appid HAVING count_7d > 50 ORDER BY count_7d DESC LIMIT 20 "; $rows = $this->query($sql); $this->sendToFeishu('📅 每日应用流量大盘', $rows, 'daily'); } protected function query(string $sql): array { return $this->clickHouseService->getClient()->select($sql)->rows(); } protected function sendToFeishu(string $title, array $rows, string $type = 'hourly') { $webhookUrl = env('FEISHU_WEBHOOK'); if (!$webhookUrl) { $this->error('未配置 FEISHU_WEBHOOK'); return; } $elements = [ ['tag' => 'markdown', 'content' => '**🕒 生成时间:** ' . now()->format('Y-m-d H:i:s')], ['tag' => 'markdown', 'content' => '**📈 应用数量:** ' . count($rows)], ['tag' => 'hr'], ]; foreach ($rows as $index => $row) { $rank = $index + 1; $appid = $row['appid'] ?: 'unknown'; if ($type === 'hourly') { $count1h = (int) $row['count_1h']; $count3h = (int) $row['count_3h']; $avg = max(1, $count3h / 3); $rate = round(($count1h / $avg) * 100); $trend = '🟢'; $status = '正常'; if ($rate >= 200) { $trend = '🚨'; $status = '流量暴涨'; } elseif ($rate >= 150) { $trend = '🟡'; $status = '流量升高'; } elseif ($rate <= 50) { $trend = '🔵'; $status = '流量下降'; } $content = "### {$trend} TOP {$rank} · {$appid}\n\n> {$status}\n\n" . "- 1小时请求量:**" . number_format($count1h) . "**\n" . "- 3小时请求量:**" . number_format($count3h) . "**\n" . "- 当前热度:**{$rate}%**"; } else { $count24h = (int) $row['count_24h']; $count3d = (int) $row['count_3d']; $count7d = (int) $row['count_7d']; $avg = max(1, $count7d / 7); $rate = round(($count24h / $avg) * 100); $trend = $rate >= 180 ? '🚨' : ($rate >= 130 ? '🟡' : '🟢'); $content = "### {$trend} TOP {$rank} · {$appid}\n\n" . "- 24小时请求量:**" . number_format($count24h) . "**\n" . "- 近3天:**" . number_format($count3d) . "**\n" . "- 近7天:**" . number_format($count7d) . "**\n" . "- 当前热度:**{$rate}%**"; } $elements[] = ['tag' => 'markdown', 'content' => $content]; $elements[] = ['tag' => 'hr']; } $elements[] = ['tag' => 'markdown', 'content' => '⚡ Powered By Laravel + ClickHouse']; $payload = [ 'msg_type' => 'interactive', 'card' => [ 'config' => ['wide_screen_mode' => true], 'header' => [ 'title' => ['tag' => 'plain_text', 'content' => $title], 'template' => 'turquoise', ], 'elements' => $elements, ], ]; $response = Http::timeout(10)->post($webhookUrl, $payload); $response->successful() ? $this->info('飞书推送成功') : $this->error('飞书推送失败: ' . $response->body()); } }注册定时任务 // routes/console.php Schedule::command('push:feishu-stats')->hourly(); Schedule::command('push:feishu-stats daily')->dailyAt('00:00');.env 配置 FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxClickHouse 性能建议 当前用 JSONExtractString(request_params, 'appid') 实时解析 JSON,数据量大时 CPU 开销高。建议在 request_logs 表冗余一个 appid String 字段,插入时直接写入,便于索引和 GROUP BY 加速。 飞书卡片效果 📊 每小时应用流量监控 🕒 生成时间:2026-05-28 10:00:00 📈 应用数量:15🚨 TOP 1 · app_main > 流量暴涨 • 1小时请求量:23,412 • 3小时请求量:41,221 • 当前热度:171%🟢 TOP 2 · app_test • 1小时请求量:1,242 • 当前热度:119%比 Markdown 表格可读性高很多,支持趋势颜色标识。