Automatic Cloudflare DNS for your Docker containers.
Deploy a container with Traefik labels β DNS just happens. No more manual CNAME records.
You deploy a new container behind Traefik. It needs a DNS record. So you:
- Open Cloudflare dashboard
- Navigate to DNS
- Create a CNAME record
- Set the target
- Toggle proxy
- Save
Every. Single. Time.
Some people have been doing this manually for years. Some have had broken auto-DNS for even longer.
CF Companion watches Docker events. When a container starts with a Traefik Host() rule, it automatically creates the Cloudflare DNS record. When you deploy 40 containers, you get 40 DNS records. Zero manual work.
Container starts β Traefik label detected β Cloudflare CNAME created β Done
services:
cf-companion:
image: smashingtags/cf-companion:latest
container_name: cf-companion
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
group_add:
- ${DOCKER_GID:-999} # Run: echo "DOCKER_GID=$(stat -c '%g' /var/run/docker.sock)" >> .env
environment:
- CF_TOKEN=your-cloudflare-api-token
- TARGET_DOMAIN=your-server.example.com
- DOMAIN1=example.com
- DOMAIN1_ZONE_ID=your-zone-id
- DOMAIN1_PROXIED=TRUE
- TRAEFIK_VERSION=2
networks:
- proxy
networks:
proxy:
external: truedocker compose up -dThat's it. Every container with a Traefik Host() label now gets automatic DNS.
docker run -d \
--name cf-companion \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--group-add $(stat -c '%g' /var/run/docker.sock) \
-e CF_TOKEN=your-cloudflare-api-token \
-e TARGET_DOMAIN=your-server.example.com \
-e DOMAIN1=example.com \
-e DOMAIN1_ZONE_ID=your-zone-id \
-e DOMAIN1_PROXIED=TRUE \
-e TRAEFIK_VERSION=2 \
--network proxy \
smashingtags/cf-companion:latestNote: The
--group-addflag is required because the container runs as a non-root user. Without it, you'll get aPermissionError(13, 'Permission denied')when the container tries to access the Docker socket.
| Registry | Image |
|---|---|
| Docker Hub | smashingtags/cf-companion:latest |
| GHCR | ghcr.io/smashingtags/cf-companion:latest |
Multi-arch: linux/amd64 and linux/arm64.
- Startup scan β discovers all running containers with Traefik routing labels
- Event watch β listens to Docker events for new container starts
- Label extraction β parses
Host()rules from Traefik v1 or v2 labels - DNS sync β creates or updates CNAME records in Cloudflare via API
- Idempotent β existing records are left alone, only missing ones are created
Supports:
- Traefik v1 and v2 label formats
- Multiple domains (DOMAIN1, DOMAIN2, DOMAIN3...)
- Docker Swarm service discovery
- Traefik API polling (alternative to Docker socket)
- Excluded subdomains
- Dry run mode
- Proxied and unproxied records
| Variable | Default | Description |
|---|---|---|
CF_TOKEN |
required | Cloudflare API token (DNS edit permission) |
CF_EMAIL |
Cloudflare email (for global API key auth) | |
TARGET_DOMAIN |
required | CNAME target (where records point) |
DOMAIN1 |
required | First domain to manage |
DOMAIN1_ZONE_ID |
required | Cloudflare Zone ID for DOMAIN1 |
DOMAIN1_PROXIED |
FALSE |
Enable Cloudflare proxy (orange cloud) |
DOMAIN1_TTL |
1 |
TTL (1 = auto) |
DOMAIN1_EXCLUDED_SUB_DOMAINS |
Comma-separated subdomains to skip | |
DOMAIN2, DOMAIN3, ... |
Additional domains (same pattern) | |
TRAEFIK_VERSION |
2 |
Traefik version (1 or 2) |
DRY_RUN |
FALSE |
Log actions without making changes |
REFRESH_ENTRIES |
FALSE |
Update records even if target matches |
RC_TYPE |
CNAME |
DNS record type |
DEFAULT_TTL |
1 |
Default TTL for all domains |
DOCKER_SWARM_MODE |
FALSE |
Enable Swarm service discovery |
ENABLE_TRAEFIK_POLL |
FALSE |
Poll Traefik API instead of Docker events |
TRAEFIK_POLL_URL |
Traefik API URL (e.g., http://traefik:8080) |
|
TRAEFIK_POLL_SECONDS |
60 |
Polling interval |
LOG_LEVEL |
INFO |
DEBUG, VERBOSE, or INFO |
LOG_TYPE |
BOTH |
CONSOLE, FILE, or BOTH |
- Go to Cloudflare API Tokens
- Create Token > Use the Edit zone DNS template
- Zone Resources: Include > Specific zone > select your domain
- Copy the token
Your Zone ID is on the right sidebar of your domain's Overview page in Cloudflare.
environment:
- DOMAIN1=example.com
- DOMAIN1_ZONE_ID=abc123
- DOMAIN1_PROXIED=TRUE
- DOMAIN2=otherdomain.com
- DOMAIN2_ZONE_ID=def456
- DOMAIN2_PROXIED=FALSEThis is a modernized fork of tiredofit/docker-traefik-cloudflare-companion. We rewrote the packaging:
| Upstream | CF Companion | |
|---|---|---|
| Base image | Proprietary tiredofit/alpine (~200MB) |
python:3.12-alpine (~50MB) |
| Init system | Custom s6-overlay with shell wrappers | Direct Python entrypoint |
| Registry | Docker Hub only | Docker Hub + GHCR |
| Architectures | amd64 only | amd64 + arm64 |
| CI/CD | Custom | GitHub Actions |
| Maintenance | Last updated April 2025 | Actively maintained |
The core Python logic is the same proven code β we just stripped out the unnecessary complexity around it.
CF Companion is built by Imogen Labs and pairs perfectly with:
- HomelabARR CE β GUI Docker container management (157 apps)
- HomelabARR β The homelab platform
Deploy containers from the HomelabARR dashboard, CF Companion handles the DNS. Fully automatic.
The container runs as a non-root user and needs access to the Docker socket. Add group_add with your host's Docker socket GID:
services:
cf-companion:
group_add:
- ${DOCKER_GID:-999}Then set the variable:
echo "DOCKER_GID=$(stat -c '%g' /var/run/docker.sock)" >> .envThanks to @cyb3rgh05t for reporting this issue.
The new Cloudflare SDK requires CF_TOKEN (scoped API token), not the legacy CF_EMAIL + CF_API_KEY combo. If you're migrating from the upstream image or DockServer:
# β Old (won't work)
- CLOUDFLARE_EMAIL=you@example.com
- CLOUDFLARE_API_KEY=your-global-key
# β
New
- CF_TOKEN=your-scoped-api-tokenCreate a scoped token with Edit zone DNS permission β see Getting a Cloudflare API Token above.
Note: Global API key auth (
CF_EMAIL+CF_API_KEY) is still supported butCF_TOKENis recommended. If using global keys, make sure the env var names areCF_EMAILandCF_API_KEY(notCLOUDFLARE_EMAIL/CLOUDFLARE_API_KEY).
Thanks to @cyb3rgh05t for reporting this issue.
- Check
LOG_LEVEL=DEBUGto see what labels are being detected - Verify your containers have Traefik
Host()labels - Confirm
DOMAIN1matches the domain in your Host rules - Make sure your API token has Edit permission on DNS for the zone
Set TARGET_DOMAIN to your server's public hostname or IP. All CNAME records will point here.
- Discord β Get help, share your setup
- Reddit β r/homelabarr
- X/Twitter β @Imogen_Labs
- Ko-fi β Support the project
MIT. Original project by Dave Conroy / tiredofit.