Gitea and Jellyfin on a Synology NAS via Cloudflare Tunnel

This is how I run a private Gitea instance and a Jellyfin media server on a Synology NAS, reachable from outside through a Cloudflare Tunnel with no firewall ports opened. Three pieces: Gitea for self-hosted Git, Jellyfin for the media library, and a Cloudflare Tunnel for access without port forwarding.

1. The architecture

A single Cloudflare Tunnel container is the gateway for all the services. They share a dedicated Docker bridge network (app_network), so the tunnel can reach Gitea and Jellyfin by container name. All traffic flows through Cloudflare’s network, so your home IP is never exposed and no router ports are opened. Each service runs in its own container.

2. The Docker Compose file

This runs all three services: Gitea, Jellyfin, and the Cloudflare Tunnel. Save it as docker-compose.yml on your Synology:

version: "3.9"

services:
  # --- Gitea (Git Service) ---
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: always
    networks:
      - app_network
    ports:
      - "3000:3000"   # Web UI
      - "2222:2222"   # SSH (use non-standard port to avoid conflicts)
    environment:
      - USER_UID=1026          # Match your Synology user UID
      - USER_GID=100           # Match your Synology user GID
      - GITEA__server__DOMAIN=gitea.yourdomain.com
      - GITEA__server__SSH_DOMAIN=git.yourdomain.com
      - GITEA__server__ROOT_URL=https://gitea.yourdomain.com/
      - GITEA__server__START_SSH_SERVER=true
      - GITEA__server__SSH_PORT=2222
    volumes:
      - /volume1/docker/gitea:/data

  # --- Jellyfin (Media Server) ---
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    networks:
      - app_network
    environment:
      - JELLYFIN_DATA_DIR=/config
      - NVIDIA_VISIBLE_DEVICES=all  # For NVIDIA GPU transcoding (optional)
    volumes:
      - /volume1/docker/jellyfin/config:/config
      - /volume1/media/video/:/media
    ports:
      - "8096:8096"

  # --- Cloudflare Tunnel ---
  tunnel:
    image: cloudflare/cloudflared:latest
    container_name: unified-tunnel
    restart: unless-stopped
    networks:
      - app_network
    command: tunnel run
    environment:
      - TUNNEL_TOKEN=${YOUR_TUNNEL_TOKEN}  # Replace with your actual token

networks:
  app_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.24.0.0/16

Before you start it: replace ${YOUR_TUNNEL_TOKEN} with your Cloudflare Tunnel token from the Zero Trust dashboard, set USER_UID and USER_GID to match your Synology user (id <username> over SSH), and update the volume paths to your own directory layout.

3. Cloudflare Tunnel configuration

To route external traffic to the services, configure the following in the Cloudflare Zero Trust dashboard under Networks > Connectors.

Public hostnames

For the tunnel, add a public hostname (application route) per service:

SubdomainService TypeInternal URLNotes
gitea.yourdomain.comHTTPhttp://gitea:3000Gitea Web UI
git.yourdomain.comTCPtcp://gitea:2222Git SSH (required for git push/pull)
jelly.yourdomain.comHTTPhttp://jellyfin:8096Jellyfin media library
internal.yourdomain.comHTTPShttps://192.168.50.2:5001Synology DSM (enable “No TLS Verify”)

Gitea needs two subdomains because a Cloudflare Tunnel routes one subdomain to one service/port. Gitea’s web UI (port 3000) and SSH (port 2222) are separate, so gitea.yourdomain.com goes to the web interface and git.yourdomain.com to SSH and Git operations.

Steps: create the tunnel in the Zero Trust dashboard and copy its token; add a public hostname for each subdomain above with the matching service type and URL; use TCP (not HTTP) for the git.yourdomain.com entry, or Git SSH fails with “bad handshake”; and enable “No TLS Verify” for the Synology DSM entry, since DSM uses a self-signed certificate.

4. Git SSH access through the tunnel

You can point your local machine at Cloudflare so Git SSH connections proxy through the tunnel automatically, without starting anything by hand before a push or pull. Add this to your SSH config (~/.ssh/config on Mac/Linux, C:\Users\<username>\.ssh\config on Windows):

Host git.yourdomain.com
    HostName %h
    User git
    Port 2222
    ProxyCommand cloudflared access tcp --hostname %h

This routes SSH connections to git.yourdomain.com through Cloudflare Access with cloudflared as a transparent proxy, so git clone git@git.yourdomain.com:username/repo.git works with nothing started by hand. You need cloudflared installed locally (download page) and authenticated once with cloudflared access login.

If a connection fails, test the tunnel directly to get detailed logs:

cloudflared access tcp --hostname git.yourdomain.com --url localhost:2222

5. Jellyfin hardware transcoding

Jellyfin runs on the Synology, so transcoding performance matters for high-resolution playback.

Direct play vs. transcoding

When the client supports the media format natively, Jellyfin direct-plays the file as-is with minimal CPU. Transcoding (4K to 1080p, or an incompatible codec) is CPU-intensive, so hardware acceleration is worth setting up.

Enabling hardware acceleration

In the Jellyfin dashboard at jelly.yourdomain.com, go to Dashboard → Playback → Transcoding and set Hardware Acceleration: “Intel QuickSync Video” (QSV) on Intel NAS models, “Video Acceleration API (VA-API)” on AMD, or “NVIDIA NVENC/NVDEC” for an NVIDIA GPU (extra setup needed).

Intel QuickSync is the common case on Synology. It covers H.264, HEVC, VP9, and AV1, handles several 4K streams at once with low CPU, and is available on most Synology models with Intel processors.

Verifying it works

Play a file, then check Dashboard → Active Devices. “Transcode Reason” should show a hardware method like “QSV”, and CPU usage should stay low during transcoding. Enabling “Prefer fMP4-HLS Media Container” in the transcoding settings improves compatibility across devices.

6. Synology firewall

The Synology firewall has to let the Docker network reach the services. In DSM → Control Panel → Security → Firewall, add a rule (if the firewall is enabled) allowing TCP ports 2222, 3000, 5001, 8096 from source IP 172.24.0.0 with netmask 255.240.0.0, which covers the Docker subnet (172.24.0.0/16). Without it, the tunnel container can’t reach Gitea and Jellyfin.

A few more things worth doing: add email or identity-provider authentication to the tunnels via Cloudflare Access policies; keep the images updated with docker-compose pull && docker-compose up -d; check container logs now and then (docker logs <container-name>); and add the Docker subnet to DSM’s Auto-Blocker allow list so the tunnel doesn’t get blocked.

Optional: Cloudflare security rules

You can also restrict access by IP or country in Cloudflare, under your domain → Security → Security rules → Custom rules → Edit custom rule.

Restrict access to your home network only:

(not ip.src in {2001:0db8:c0de:cafe::/64} and http.host wildcard "gitea.yourdomain.com") 
or (http.host wildcard "jelly.yourdomain.com" and not ip.src in {2001:0db8:c0de:cafe::/64}) 
or (http.host wildcard "internal.yourdomain.com" and not ip.src in {2001:0db8:c0de:cafe::/64})

This blocks access unless the request comes from your IPv6 subnet. Replace 2001:0db8:c0de:cafe::/64 with your home network prefix.

Block specific countries (for example to cut down on automated attacks):

(ip.geoip.country in {"RU" "CN" "KP"}) and http.host wildcard "*.yourdomain.com"

You can combine the two with and/or, for example allowing your home IP while blocking certain countries for everything else.

Next steps

From here you might set up automated backups for the Gitea repositories and Jellyfin metadata, add Cloudflare Access policies for authentication, drop more services into the docker-compose.yml (Nextcloud, Bitwarden, Home Assistant), and keep an eye on resource usage.

How did you like this article?