From 566b15891f2bbfffd971b8a8d9fa07bd35ad385a Mon Sep 17 00:00:00 2001 From: Alex N <1434629+santaklouse@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:17:06 +0300 Subject: [PATCH 1/2] Add Docker Compose deployment with Cloudflare Tunnel --- .dockerignore | 13 +++++++ .env.example | 2 + .gitignore | 1 + Dockerfile | 26 +++++++++++++ README.md | 69 ++++++++++++++++++++++++++++++++++ compose.yml | 59 +++++++++++++++++++++++++++++ docker/config.json | 42 +++++++++++++++++++++ docker/profiles.json | 10 +++++ docker/site/404.html | 13 +++++++ docker/site/index.html | 14 +++++++ internal/config/config.go | 17 +++++++-- internal/config/config_test.go | 15 ++++++++ 12 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 compose.yml create mode 100644 docker/config.json create mode 100644 docker/profiles.json create mode 100644 docker/site/404.html create mode 100644 docker/site/index.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..033df0f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.gitignore +Dockerfile +compose.yml +README.md +*.md +config.json +profiles.json +websites +coverage.out +*.test +*.prof +.DS_Store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4ec2fe2 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +TPROXY_HOSTNAME=proxy.example.com +CLOUDFLARE_TUNNEL_TOKEN= diff --git a/.gitignore b/.gitignore index 32cd33d..4d647fd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /*.test /*.prof .DS_Store +/.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..93be5be --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.25-alpine AS build + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY cmd ./cmd +COPY internal ./internal +RUN CGO_ENABLED=0 GOOS=linux go build \ + -trimpath \ + -ldflags="-s -w" \ + -o /out/tproxy-server \ + ./cmd/tproxy-server + +FROM alpine:3.22 + +RUN apk add --no-cache ca-certificates \ + && addgroup -S -g 10001 tproxy \ + && adduser -S -D -H -u 10001 -G tproxy tproxy + +COPY --from=build /out/tproxy-server /usr/local/bin/tproxy-server + +USER 10001:10001 +ENTRYPOINT ["/usr/local/bin/tproxy-server"] +CMD ["-config", "/etc/tproxy-server/config.json"] diff --git a/README.md b/README.md index 72d7cf5..e48cc3d 100644 --- a/README.md +++ b/README.md @@ -600,3 +600,72 @@ when overriding the defaults. The complete architecture and implementation milestones remain in `PLAN.md`; the normative wire format is in `PROTOCOL.md`. + +## Docker Compose with Cloudflare Tunnel + +The Compose stack publishes no host ports. The relay listens on port 8080 only +inside the private `tunnel` network, and `cloudflared` is the only HTTP ingress. +Cloudflared also joins a separate outbound network so it can reach Cloudflare; +the relay itself has no direct Internet egress. +The admin listener remains on loopback inside the relay container and is not +reachable from the tunnel container. + +Before the first start, copy `.env.example` to `.env`, set the token issued on the +Cloudflare Zero Trust tunnel page, and set the same public hostname in +`TPROXY_HOSTNAME` and `public_hostname` inside `docker/config.json`. Replace the +example secret in `docker/profiles.json` with the same 16-byte hexadecimal +MTProxy secret used by the client: + +```bash +cp .env.example .env +docker compose config +docker compose build +docker compose run --rm tproxy-server -config /etc/tproxy-server/config.json -check +docker compose up -d +docker compose ps +docker compose logs --tail=100 tproxy-server +docker compose logs --tail=100 cloudflared +``` + +In the Cloudflare tunnel configuration, create a Public Hostname whose service is +`http://tproxy-server:8080`. Cloudflare terminates public HTTPS; Caddy is not part +of this stack. Do not add a Compose `ports` mapping for the relay. + +### Настройка + +Создайте `.env`: + +```bash +cp .env.example .env +``` + +Заполните его: + +```dotenv +TPROXY_HOSTNAME=proxy.example.com +CLOUDFLARE_TUNNEL_TOKEN=токен_выданный_cloudflare +``` + +Такой же домен укажите в [`docker/config.json`](docker/config.json): + +```json +"public_hostname": "proxy.example.com" +``` + +В Cloudflare для Public Hostname укажите сервис: + +```text +http://tproxy-server:8080 +``` + +Запуск: + +```bash +docker compose down --remove-orphans +docker compose up -d --build +docker compose ps +docker compose logs --tail=100 cloudflared +docker compose logs --tail=100 tproxy-server +``` + +Порты 80, 443 и 8080 на целевом хосте открывать или публиковать не требуется. diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..6b94a2f --- /dev/null +++ b/compose.yml @@ -0,0 +1,59 @@ +services: + tproxy-server: + build: + context: . + image: tproxy-server:local + container_name: tproxy-server + restart: unless-stopped + init: true + read_only: true + user: "10001:10001" + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + expose: + - "8080" + volumes: + - ./docker/config.json:/etc/tproxy-server/config.json:ro + - ./docker/site:/srv/tproxy-site:ro + secrets: + - source: tproxy_profiles + target: tproxy-profiles.json + healthcheck: + test: ["CMD", "/usr/local/bin/tproxy-server", "-config", "/etc/tproxy-server/config.json", "-check"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 5s + stop_grace_period: 20s + networks: + - tunnel + - cloudflare + + cloudflared: + image: cloudflare/cloudflared:2026.8.0 + container_name: tproxy-cloudflared + restart: unless-stopped + init: true + read_only: true + user: "65532:65532" + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN:?set CLOUDFLARE_TUNNEL_TOKEN in .env} + depends_on: + tproxy-server: + condition: service_healthy + networks: + - tunnel + +secrets: + tproxy_profiles: + file: ./docker/profiles.json + +networks: + tunnel: + internal: true + cloudflare: diff --git a/docker/config.json b/docker/config.json new file mode 100644 index 0000000..b8e1a61 --- /dev/null +++ b/docker/config.json @@ -0,0 +1,42 @@ +{ + "public_hostname": "proxy.example.com", + "listen": "0.0.0.0:8080", + "admin_listen": "127.0.0.1:8081", + "public_dir": "/srv/tproxy-site", + "profiles_file": "/run/secrets/tproxy-profiles.json", + "enable_pprof": false, + "limits": { + "max_header_bytes": 16384, + "max_body_bytes": 2097152, + "max_frame_payload": 1048576, + "carrier_batch_bytes": 2097152, + "max_streams_per_session": 128, + "max_closed_stream_ids": 4096, + "max_pending_per_session": 33554432, + "max_pending_global": 536870912, + "max_pending_items_per_session": 16384, + "max_pending_items_global": 262144, + "max_sessions_per_ip": 0, + "max_sessions_global": 128, + "max_streams_global": 4096, + "max_backend_dials_in_flight": 256, + "new_sessions_per_minute": 600, + "new_sessions_burst": 128, + "new_streams_per_minute": 6000, + "new_streams_burst": 512, + "max_bootstraps_per_ip": 0, + "max_bootstraps_global": 512, + "new_bootstraps_per_minute": 1200, + "new_bootstraps_burst": 256, + "max_profiles": 32 + }, + "timeouts": { + "backend_dial": "5s", + "long_poll": "25s", + "reconnect_grace": "2m", + "bootstrap_lifetime": "2m", + "read_header": "10s", + "idle": "75s", + "shutdown": "15s" + } +} diff --git a/docker/profiles.json b/docker/profiles.json new file mode 100644 index 0000000..8bd5474 --- /dev/null +++ b/docker/profiles.json @@ -0,0 +1,10 @@ +{ + "profiles": [ + { + "name": "default", + "secret": "0123456789abcdef0123456789abcdef", + "backend": "127.0.0.1:2398", + "carrier_mode": "https" + } + ] +} diff --git a/docker/site/404.html b/docker/site/404.html new file mode 100644 index 0000000..a1d4ed4 --- /dev/null +++ b/docker/site/404.html @@ -0,0 +1,13 @@ + + +
+ + +This website is available.
+