{"id":43153948,"url":"https://github.com/darkdragon14/docker-cloudflare-tunnel-sync","last_synced_at":"2026-04-27T19:01:16.779Z","repository":{"id":334437735,"uuid":"1137746044","full_name":"Darkdragon14/docker-cloudflare-tunnel-sync","owner":"Darkdragon14","description":"Automatically synchronize Cloudflare Tunnels and routes from Docker container labels.","archived":false,"fork":false,"pushed_at":"2026-04-07T18:55:42.000Z","size":6023,"stargazers_count":38,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-07T20:32:24.886Z","etag":null,"topics":["automation","cloudflare","cloudflare-tunnel","cloudflared","devops","docker","infrastructure","labels","self-hosted","service-discovery","zero-trust"],"latest_commit_sha":null,"homepage":"https://darkdragon14.github.io/docker-cloudflare-tunnel-sync/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Darkdragon14.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-19T19:25:58.000Z","updated_at":"2026-04-07T18:51:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Darkdragon14/docker-cloudflare-tunnel-sync","commit_stats":null,"previous_names":["darkdragon14/docker-cloudflare-tunnel-sync"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/Darkdragon14/docker-cloudflare-tunnel-sync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkdragon14%2Fdocker-cloudflare-tunnel-sync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkdragon14%2Fdocker-cloudflare-tunnel-sync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkdragon14%2Fdocker-cloudflare-tunnel-sync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkdragon14%2Fdocker-cloudflare-tunnel-sync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Darkdragon14","download_url":"https://codeload.github.com/Darkdragon14/docker-cloudflare-tunnel-sync/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkdragon14%2Fdocker-cloudflare-tunnel-sync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32350243,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-27T17:12:42.749Z","status":"ssl_error","status_checked_at":"2026-04-27T17:12:41.658Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","cloudflare","cloudflare-tunnel","cloudflared","devops","docker","infrastructure","labels","self-hosted","service-discovery","zero-trust"],"created_at":"2026-02-01T00:30:34.600Z","updated_at":"2026-04-27T19:01:16.773Z","avatar_url":"https://github.com/Darkdragon14.png","language":"Go","readme":"# docker-cloudflare-tunnel-sync\n\n[![Release](https://img.shields.io/github/v/release/Darkdragon14/docker-cloudflare-tunnel-sync?sort=semver)](https://github.com/Darkdragon14/docker-cloudflare-tunnel-sync/releases/latest) [![Tests](https://github.com/Darkdragon14/docker-cloudflare-tunnel-sync/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/Darkdragon14/docker-cloudflare-tunnel-sync/actions/workflows/tests.yml) [![Container](https://github.com/Darkdragon14/docker-cloudflare-tunnel-sync/actions/workflows/ghcr.yml/badge.svg?branch=main)](https://github.com/Darkdragon14/docker-cloudflare-tunnel-sync/actions/workflows/ghcr.yml)\n\n\u003e Turn Docker labels into Cloudflare Tunnel routes, DNS records, and Access rules.\n\nStop managing Cloudflare dashboards by hand.  \nLet your containers be the source of truth.\n\n\u003e **Disclaimer:** Use a dedicated Cloudflare Tunnel for this controller. If you attach it to an existing tunnel that already has published application routes, enabling managed sync can delete those routes.\n\n---\n\n## ✨ Why this exists\n\nManaging Cloudflare Tunnel routes manually does not scale.\n\n- Routes drift over time\n- Old services stay exposed\n- Access rules become outdated\n- Documentation gets ignored\n\nThis project solves that by syncing Cloudflare configuration directly from Docker container labels.\n\nIf a container exists → it is exposed.  \nIf it disappears → Cloudflare is cleaned up.\n\nNo manual work. No drift.\n\n---\n\n## 🚀 What it does\n\n`docker-cloudflare-tunnel-sync` continuously reconciles:\n\n- ✅ Tunnel ingress rules\n- ✅ DNS records\n- ✅ Cloudflare Access applications \u0026 policies (optional)\n\nfrom Docker labels.\n\nDocker becomes your single source of truth.\n\n---\n\n## 🧩 How it works\n\n```mermaid\nflowchart LR\n  A[Docker labels] --\u003e B[docker-cloudflare-tunnel-sync]\n  B --\u003e C[Cloudflare Tunnel]\n  B --\u003e D[Cloudflare DNS]\n  B --\u003e E[Cloudflare Access]\n```\n\n1. The controller watches Docker events\n2. Reads container labels\n3. Translates them into Cloudflare resources\n4. Reconciles differences\n5. Removes stale config automatically\n\n---\n\n## 📦 Quickstart\n\n### 1. Create a Cloudflare API token\n\nRequired permissions:\n\n| Scope | Resource | Access |\n|----------|-------------|---------|\n| Account | Cloudflare Tunnel | Edit |\n| Account | Access: Apps and Policies | Edit |\n| Zone | Zone | Read |\n| Zone | DNS | Edit |\n\n\u003e ⚠️ Do not use a Global API Key. Always use a scoped token with the minimum required permissions.\n\n---\n\n### 2. Run the controller\n\nPull the image:\n\n```\ndocker pull ghcr.io/darkdragon14/docker-cloudflare-tunnel-sync\n```\n\nRun with Docker:\n\n```bash\ndocker run --rm \\\n  -e CF_API_TOKEN=your-token \\\n  -e CF_ACCOUNT_ID=your-account-id \\\n  -e CF_TUNNEL_ID=your-tunnel-id \\\n  -e SYNC_MANAGED_TUNNEL=true \\\n  -e SYNC_MANAGED_ACCESS=true \\\n  -e SYNC_MANAGED_DNS=true \\\n  -e SYNC_DELETE_DNS=true \\\n  -e SYNC_POLL_INTERVAL=30s \\\n  -v /var/run/docker.sock:/var/run/docker.sock:ro \\\n  ghcr.io/darkdragon14/docker-cloudflare-tunnel-sync\n```\n\n\u003e ⚠️ The Docker socket is mounted read-only for safety.\n\nDocker secrets are also supported for the sensitive Cloudflare values. The controller checks `/run/secrets/\u003cVARIABLE\u003e` before falling back to the matching environment variable.\n\nExample with Docker Compose:\n\n```yaml\nservices:\n  docker-cloudflare-tunnel-sync:\n    image: ghcr.io/darkdragon14/docker-cloudflare-tunnel-sync\n    secrets:\n      - CF_API_TOKEN\n      - CF_ACCOUNT_ID\n      - CF_TUNNEL_ID\n    environment:\n      SYNC_MANAGED_TUNNEL: \"true\"\n      SYNC_MANAGED_ACCESS: \"true\"\n      SYNC_MANAGED_DNS: \"true\"\n      SYNC_DELETE_DNS: \"true\"\n      SYNC_POLL_INTERVAL: 30s\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n\nsecrets:\n  CF_API_TOKEN:\n    file: ./secrets/cf_api_token\n  CF_ACCOUNT_ID:\n    file: ./secrets/cf_account_id\n  CF_TUNNEL_ID:\n    file: ./secrets/cf_tunnel_id\n```\n\n---\n\n### 3. Label your containers\n\nExample:\n\n```yaml\nservices:\n  app:\n    image: nginx\n    labels:\n      cloudflare.tunnel.enable: \"true\"\n      cloudflare.tunnel.hostname: app.example.com\n      cloudflare.tunnel.service: http://app:80\n```\n\nStart the container — it is automatically exposed.\n\n---\n\n## ⚙️ Configuration\n\n### Environment variables\n\n| Variable | Required | Default | Description |\n| --- | --- | --- | --- |\n| `CF_API_TOKEN` | yes | - | Cloudflare API token with Account permissions (`Cloudflare Tunnel:Edit`, plus `Access Apps and Policies:Edit` for Access labels) and Zone permissions (`Zone:Read` + `DNS:Edit` for DNS automation). |\n| `CF_ACCOUNT_ID` | yes | - | Cloudflare account identifier. |\n| `CF_TUNNEL_ID` | yes | - | Cloudflare Tunnel identifier. |\n| `CF_API_BASE_URL` | no | `https://api.cloudflare.com/client/v4` | Override Cloudflare API base URL. |\n| `DOCKER_HOST` | no | - | Docker daemon host (standard Docker env var). |\n| `DOCKER_API_VERSION` | no | - | Docker API version override. |\n| `SYNC_POLL_INTERVAL` | no | `30s` | Controller poll interval. |\n| `SYNC_RUN_ONCE` | no | `false` | Run a single reconciliation and exit. |\n| `SYNC_DRY_RUN` | no | `false` | Log changes without applying them. |\n| `SYNC_MANAGED_TUNNEL` | no | `false` | Allow this tool to overwrite the tunnel ingress configuration. |\n| `SYNC_MANAGED_ACCESS` | no | `false` | Allow this tool to create/update Access apps and policies. |\n| `SYNC_MANAGED_DNS` | no | `false` | Allow this tool to create/update DNS CNAME records for tunnel hostnames. |\n| `SYNC_DNS_ZONES` | no | - | Comma-separated DNS zones to keep scanning for orphan cleanup when `SYNC_DELETE_DNS=true`, even if no current labels resolve to those zones. |\n| `SYNC_DELETE_DNS` | no | `false` | Delete managed DNS records in zones selected from current labels plus any zones listed in `SYNC_DNS_ZONES`. This does not perform a full account-wide cleanup. |\n| `SYNC_MANAGED_BY` | no | `docker-cf-tunnel-sync` | Override the managed-by tag/comment value (used for Access tags and DNS comments). |\n| `LOG_LEVEL` | no | `info` | `debug`, `info`, `warn`, or `error`. |\n\nFor `CF_API_TOKEN`, `CF_ACCOUNT_ID`, and `CF_TUNNEL_ID`, required means the value must be provided either as an environment variable or as a Docker secret.\n\n### Docker secrets\n\nThe following sensitive values can be provided either as environment variables or Docker secrets mounted at `/run/secrets/\u003cVARIABLE\u003e`. Docker secrets take precedence when present and non-empty.\n\n| Secret | Required | Description |\n| --- | --- | --- |\n| `CF_API_TOKEN` | yes | Cloudflare API token. |\n| `CF_ACCOUNT_ID` | yes | Cloudflare account identifier. |\n| `CF_TUNNEL_ID` | yes | Cloudflare Tunnel identifier. |\n\n---\n\n### Labels\n\nAll labels are explicit and namespaced. A container is only managed when `cloudflare.tunnel.enable=true`.\n\n| Label | Required | Example | Description |\n| --- | --- | --- | --- |\n| `cloudflare.tunnel.enable` | yes | `true` | Opt-in flag for route creation. |\n| `cloudflare.tunnel.hostname` | yes | `app.example.com` | Base route hostname (required). |\n| `cloudflare.tunnel.service` | yes | `http://api:8080` | Base route service/origin URL (required). |\n| `cloudflare.tunnel.dns.zone` | no | `dev.example.com` | Override automatic DNS zone selection for this route hostname. Useful when Cloudflare manages a delegated sub-zone. |\n| `cloudflare.tunnel.path` | no | `/api` | Optional base route path prefix (must start with `/`). |\n| `cloudflare.tunnel.origin.server-name` | no | `app.internal` | Optional base route `originRequest.originServerName` (TLS SNI override). |\n| `cloudflare.tunnel.origin.no-tls-verify` | no | `true` | Optional base route `originRequest.noTLSVerify` (`true`/`false`). |\n\n\u003e **Note - Additional routes by suffix**\n\u003e\n\u003e The base route labels `cloudflare.tunnel.hostname` and `cloudflare.tunnel.service` remain required for every managed container.\n\u003e\n\u003e You can define additional routes with suffix-based labels:\n\u003e - `cloudflare.tunnel.hostname.\u003csuffix\u003e`\n\u003e - `cloudflare.tunnel.service.\u003csuffix\u003e`\n\u003e - `cloudflare.tunnel.dns.zone.\u003csuffix\u003e`\n\u003e - `cloudflare.tunnel.path.\u003csuffix\u003e`\n\u003e - `cloudflare.tunnel.origin.server-name.\u003csuffix\u003e`\n\u003e - `cloudflare.tunnel.origin.no-tls-verify.\u003csuffix\u003e`\n\u003e\n\u003e A suffix route is created only when both `hostname.\u003csuffix\u003e` and `service.\u003csuffix\u003e` are set.\n\u003e If one is missing, the controller logs a warning and skips that suffix.\n\u003e Empty suffix labels (for example `cloudflare.tunnel.hostname.`) are ignored.\n\nWhen either origin label is omitted for a managed route, the corresponding `originRequest` key is removed during reconciliation. Unmanaged `originRequest` keys are preserved.\n\nDNS sync derives the target zone automatically from each hostname using the effective eTLD+1. For example, `app.dev.example.com` defaults to `example.com`. Set `cloudflare.tunnel.dns.zone` (or `cloudflare.tunnel.dns.zone.\u003csuffix\u003e`) to target a more specific Cloudflare zone such as `dev.example.com`.\n\nThe DNS engine only queries zones selected by these rules. When `SYNC_DELETE_DNS=true`, you can extend that scan scope with `SYNC_DNS_ZONES`. This is useful when an entire zone disappears from current labels but you still want the controller to delete old managed DNS records in that zone.\n\nExample:\n\n```bash\n-e SYNC_MANAGED_DNS=true \\\n-e SYNC_DELETE_DNS=true \\\n-e SYNC_DNS_ZONES=darkdragon.fr,cf.darkdragon.fr\n```\n\n`cloudflare.tunnel.dns.zone` selects the Cloudflare zone for a specific hostname. `SYNC_DNS_ZONES` is different: it only keeps whole zones in the cleanup scan set when deleting orphaned DNS records.\n\n### Access labels\n\nAccess applications are only managed when `cloudflare.access.enable=true`. Policy indices (`policy.1`, `policy.2`, etc.) define evaluation order. Comma-separated lists are accepted for emails, IPs, and tags. If only `policy.N.id` or `policy.N.name` is provided, the policy is referenced without updates. If `cloudflare.access.app.domain` is omitted, the controller uses `cloudflare.tunnel.hostname`. When `cloudflare.access.app.tags` is set, the controller ensures those tags exist (creating them if needed) and manages app tags to match that list (plus the managed-by tag when `SYNC_MANAGED_ACCESS=true`); if omitted, existing tags are preserved.\n\n| Label | Required | Example | Description |\n| --- | --- | --- | --- |\n| `cloudflare.access.enable` | yes | `true` | Opt-in flag for Access management. |\n| `cloudflare.access.app.name` | yes | `nginx` | Access application name. |\n| `cloudflare.access.app.domain` | yes* | `nginx.example.com` | Access application domain (required unless `cloudflare.tunnel.hostname` is set). |\n| `cloudflare.access.app.id` | no | `app-uuid` | Optional existing app ID to update. |\n| `cloudflare.access.app.tags` | no | `team,internal` | Comma-separated Access app tags; when set, missing tags are created and the list is enforced. |\n| `cloudflare.access.policy.1.name` | yes* | `allow-team` | Policy name (required unless using ID-only reference; if set without other policy fields, the policy is referenced by name). |\n| `cloudflare.access.policy.1.action` | yes* | `allow` | Policy action (`allow` or `deny`, required unless using reference-only mode). |\n| `cloudflare.access.policy.1.include.emails` | no | `me@example.com` | Comma-separated allowed emails. |\n| `cloudflare.access.policy.1.include.ips` | no | `192.0.2.0/24` | Comma-separated allowed IPs/CIDRs. |\n| `cloudflare.access.policy.1.id` | no | `policy-uuid` | Optional existing policy ID. If set without other policy fields, the policy is referenced only and not updated (same behavior for name-only references). |\n\nWhen no app or policy ID is provided, the controller matches existing resources by name (and domain for apps); if multiple matches exist, reconciliation is skipped with a warning. Name-only policy references must match an existing policy. If a policy ID is provided but not found in account-level policies, the controller will still attach the ID (useful for app-scoped policies).\n\n\n---\n\n## 🔐 Security model\n\nThis project never exposes services by default.\n\nOnly containers with explicit labels are managed.\n\n### Docker socket\n\n- Mounted read-only\n- Used only to read metadata\n- No container control\n\n### Cloudflare token\n\nUse scoped tokens.\n\nDo **not** use global API keys.\n\n---\n\n## 🛟 Safe mode\n\nWhen enabled, the controller:\n\n- Never deletes existing Cloudflare resources\n- Only logs planned changes\n\nUseful for:\n\n- First deployment\n- Testing\n- Production audits\n\n```bash\n-e SYNC_DRY_RUN=true\n```\n\n---\n\n## 🗺️ Roadmap\n\nPlanned improvements:\n\n- [ ] Label validation\n- [ ] Web UI (optional)\n\n---\n\n## 🤝 Contributing\n\nPRs and issues are welcome.\n\nIf you plan major changes, please open a discussion first.\n\n## 🤝 Contributors\n\n- [Warren Noronha (@wnoronha)](https://github.com/wnoronha) - Docker secrets support.\n\n---\n\n## 📄 License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkdragon14%2Fdocker-cloudflare-tunnel-sync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdarkdragon14%2Fdocker-cloudflare-tunnel-sync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkdragon14%2Fdocker-cloudflare-tunnel-sync/lists"}