Setup a Cloudflare Tunnel to securely access self-hosted apps with a domain from outside the home network
self-hosting
Cloudflare Tunnels have been around for a few years and are well regarded alternatives for VPNs or port-forwarding on a router. They are often used to expose access to self-hosted apps from outside the local network with minimal config or hassle. Here's how it's done.
Sections
- Pre-Requisites
- Add a domain to Cloudflare
- Create the Cloudflare Tunnel
- Configure OAuth with Google
- Create an access policy
- References
Pre-Requisites
This guide is assumes you already have a Linux machine with Docker installed and a container you want to expose up and running. For the examples below I’ll be using Navidrome because that’s what I set this up for in the first place and it just works. However, most self-hosted apps should work the same if you access it at, for example, 192.168.0.200:4533
.
Apps that have to be accessed through a specific path (like /admin
or /web
) and have no redirect from the index page may act weird when it comes time to proxy the tunnel. I always run into this issue with Ubooquity in particular and haven’t figured out how to fix it. I won’t be trying to deal with that here.
The process of setting up and securing a Cloudflare Tunnel is a lot of steps, so I’m basically paraphrasing the Cloudflare Zero Trust Docs. When in doubt, refer back to them.
![]()
I usually try to include images when making guides, but this is already going to be huge and I try to be as terse as possible. As a result there’s no pretty pictures in this post, just a lot of reading step-by-step instructions on configuring various settings.
Add a domain to Cloudflare
This assumes you already own a domain from another registrar, like Namecheap or Porkbun, and just want to add it to Cloudflare. Alternately, you can register a new domain with Cloudflare, but I’m not going over how to do that here. It’s pretty easy, anyway.
-
Login to Cloudflare, go to Websites on the sidebar if you’re not already there, and click the Add a site button.
-
Enter your domain and click Add site, then click on the Free plan at the bottom and click Continue.
-
The waiting a few moments for the DNS quick scan, you should see your domain’s DNS records appear. Click on Continue.
-
Cloudflare will now present you with the URLs to two nameservers, should be something like
adam.ns.cloudflare.com
. Leave this page open, we’ll come back to it. -
Login to the registrar that owns your domain, go into your domain’s settings, and change the DNS nameservers to both of the URLs provided by Cloudflare.
I tend to use Namecheap, so I can tell you if your domain is with them, go to Domain List and click Manage next to the domain you want to add. Next to Nameservers choose Custom DNS from the dropdown list, add the two Cloudflare nameservers, and click the green checkmark to finish.
-
Back in Cloudflare, click Done, check nameservers. It could take up to 24 hours for the change to propagate, but usually it will take less than an hour, and often less than 20 minutes. In the meantime, follow the Quick Start Guide.
-
Leave Automatic HTTPS Rewrites checked as-is, and activate the checkbox for Always Use HTTPS.
-
Leave Brotli on. On the summary, click Finished.
-
You’ll be back at your site’s Overview. If you still see
Complete your nameserver setup
, you can try using the Check nameservers button. In my experience that makes the DNS changes occur within a few minutes.
Once your DNS changes have propagated, the Overview page will say: “Great news! Cloudflare is now protecting your site!” That means you’re good to go.
Create the Cloudflare Tunnel
Go to the Cloudflare Zero Trust dashboard by clicking Access on the sidebar, then click on Launch Zero Trust to open it in a new tab. Once at the dashboard, do the following:
-
On the sidebar, go to Network -> Tunnels.
-
Click the Create a tunnel button, give it a name, and click Save tunnel.
-
The next page gives you instructions on how to run the connector. It varies based on how you want to do it, but is self-explanatory. Copy and paste the command provided into your server’s terminal to run
cloudflared
. Give it a minute or two, then check Cloudflare — reload the page if necessary or wait a few minutes, your tunnel will eventually show as Healthy status. Once it does, move on to the next step.
![]()
If want to run
cloudflared
as a container with Docker Compose (rather than thedocker run
command), copy and paste the command into Composerize. Or you can just check out this gist I made to run Navidrome and Cloudflared together as a stack, which is how I have it set up. (That way I can start up and take down the stack as needed withdocker compose up
anddocker compose down
, or with one click through Portainer.)
-
Next you’ll be on the Route traffic page. Under Public hostnames type in your sub-domain (i.e.
music
) and then your domain. Below that under Services, for Type choose HTTP (not HTTPS), and for URL enter your local IP address and port of the service you”re exposing, e.g.192.168.0.200:4533
. To finish, click the Save tunnel button. -
Now, to verify everything is working as intended, go to DNS -> Records on the sidebar. You should see a
CNAME
record pointing themusic
sub-domain to a URL like5e126941-1234-8e13-4d80-02fe21084a62.cfargotunnel.com
. (The alpha-numeric string is your tunnel ID.)
You should be done! Go to https://music.your-domain.com
and you should reach the Navidrome UI! However, you’re not the only one with access, technically anyone with the URL can reach it unabated.
Although Navidrome, like many self-hosted services, has username and passwords for login, you can also put authentication services in front of the tunnel to stop unauthorized visitors from even reaching your app. Read on…
Configure OAuth with Google
![]()
You’ll need a Google account to set this up, which you already do with Gmail. You’ll be using that email to do some stuff on Google Cloud Platform. It’s totally free for what we’re doing.
-
Go to Google Cloud Platform and go to Console at the top-right. On the next page click the dropdown menu at the top-left and go to New Project. Name the project and click Create.
-
On the project home page, go to APIs & Services on the sidebar and select Dashboard.
-
On the sidebar, go to Credentials and select Configure Consent Screen at the top of the page.
-
Choose
External
as the User Type. Since this application is not being created in a Google Workspace account, any user with a Gmail address can login. -
Name the application, add a support email, and input contact fields. Google Cloud Platform requires an email in your account.
-
(Optional) In the Scopes section, add the
userinfo.email
scope. This is not required for the integration, but shows authenticating users what information is being gathered. -
Return to the APIs & Services page, select Create Credentials -> OAuth client ID, and name the application.
-
Under Authorized JavaScript origins, in the URIs field, enter your team domain. For example:
https://<your-team-name>.cloudflareaccess.com
If you don’t know it, go to the Cloudflare Zero Trust dashboard -> Settings -> Custom Pages to see your team domain.
-
Under Authorized redirect URIs, in the URIs field, enter your team domain followed by this callback at the end of the path:
https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback
-
Google will present the OAuth Client ID and Secret values. The secret field functions like a password and should not be shared. Copy both values.
-
In Zero Trust, go to Settings -> Authentication.
-
Under Login methods, select Add new. Choose Google on the next page.
-
Input the Client ID and Client Secret fields generated by Google Cloud Platform previously.
-
(Optional) Enable Proof of Key Exchange (PKCE). PKCE will be performed on all login attempts.
-
Select Save. Make sure to use the Test link to make sure it works.
Create an access policy
Now that the OAuth provider is set up, we need make use of it with Access Policies.
-
In Zero Trust, go to Access -> Applications -> Add an application.
-
Select Self-Hosted. Under Application Configuration, name the application
Navidrome
and choose session duration. Add the sub-domain you want to use (e.g.music
) and the domain you transferred to Cloudflare earlier.
![]()
Ignore the warning about no DNS record found for this domain. Cloudflare is complaining about no A/AAAA records, but we don’t need them for access via Tunnel.
-
(Optional) Leave the Application Appearance the same, and if you’d like select Custom Logo and paste in the Navidrome logo URL:
https://raw.githubusercontent.com/navidrome/navidrome/master/resources/logo-192x192.png
-
Leave the Block pages set to
Cloudflare default
, add a Cloudflare error text if you’d like. (It has to be 75 characters or less.) -
Under Identity providers uncheck
Accept all available identity providers
, then checkInstant Auth
. -
Click the Next button at the bottom-right.
-
Type in a Policy name, as Action choose
Allow
, and leave Session duration as-is. -
Under Configure rules -> Include, select Email and add an email address to Value.
-
(Optional) If you like even more security, click + Add require and choose Country as selector and your home country as the Value.
-
(Optional) You can activate Purpose justification which apparently emails an approval email each time there is a login, like a 2 factor auth. I don’t bother with this, so I don’t really know.
-
Click the Next button.
-
Unless you know what you’re doing, leave the all the additional settings alone. Just scroll to the bottom and click the Add application button to finish.
Success!
Now when you go to https://music.your-domain.com
you should be met with a Google account login page. Login with the email you added and you should hit the Navidrome UI.
![]()
If you get any DNS errors when trying to access your domain after adding OAuth with the above steps, but didn’t have any issues before that, you may be hitting your ad blocker. I didn’t have issues with Pi-Hole, but when testing behind NextDNS I did get
NXDOMAIN
errors.
See this post on NextDNS Help Center. TLDR: Try disabling DNSSEC for your domain on the Cloudflare dashboard and see if that resolves the issue. (I have not tested it.)