DEV Community

Voke
Voke

Posted on

How I Exposed a Local Dockerized MERN App Using Tailscale (Without Domains or Port Forwarding)

Table of Contents

The Overview:

In this piece, I will walk through how I exposed a fully built and Dockerized MERN application using Tailscale.
The application itself was already complete, so this article focuses on the infrastructure decision and setup, not application development.

While installing Tailscale, I encountered an unexpected TLS error, despite my internet connection being functional. That debugging journey is what this write-up is really about. Well, and Other issues too.

So, let’s get right into it.

What is Tailscale?

Tailscale is a secure networking tool that enables devices, such as laptops, phones, and servers, to communicate over the internet as if they were on the same private local network.

Instead of exposing your app to the public internet or dealing with complex VPN setups, Tailscale gives each device a private IP address and connects them directly. This means we can run MERN apps or web apps locally or on a private server and still access them from anywhere, without public IPs, port forwarding, or buying a domain.

Now that we have got that out of the way, how on earth did I come about Tailscale?

How I came about Tailscale?

Before December 2025, I had never heard of Tailscale. I stumbled on it through a YouTube video (link below), got curious, and decided to see if it could solve a real problem I was facing: exposing my web app for remote access without using public IPs.

That curiosity led to an unexpected networking issue and a useful learning experience.

Link here:

https://youtube.com/shorts/n8C1SS-Z4L8?si=bS31jYL-NrWPTEdL 
Enter fullscreen mode Exit fullscreen mode

Environment Setup:

At this point, the application itself was already complete.
All Docker images had been pulled from Docker Hub, and the project was ready to run.

The setup was simple:

Host Os: Windows

Linux Environment: Ubuntu via WSL2

Container Runtime: Docker Desktop

The web application runs inside the Ubuntu environment located at:

voke@DESKTOP-V2*****:~/homelab-server/deploy$
Enter fullscreen mode Exit fullscreen mode

So normally, if I run:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

This would bring up all containers successfully. Because the application sits behind Nginx, it would be accessible locally on:

http://localhost/
Enter fullscreen mode Exit fullscreen mode

At this stage, everything worked, but only locally.

To access the application from outside my machine, I needed a secure networking layer. That’s where Tailscale came in.

Tailscale Installation: First Attempt:

So I attempted to install Tailscale using the official install script:

curl -fsSL https://tailscale.com/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

This command downloads and executes Tailscale’s installation script, allowing the tool to be installed without manual configuration.

However, the installation failed with the following error:

curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to tailscale.com:443 
Enter fullscreen mode Exit fullscreen mode

Damnnn!!! That hit me hard, as I wasn't expecting such. I was expecting a smooth sail, only for me to hit an iceberg like the Titanic. Well, in programming and Development as a whole, anything can happen. And that’s no reference to the Refugee Camp All-Stars and Wyclef Jean's hit song from 1997.

So that error above indicates that curl was unable to establish a secure HTTPS connection to tailscale.com.

In most cases, such an error is caused by either of the following:

  • Network filtering or blocked HTTPS traffic.

  • DNS resolution issues

  • TLS interception or inspection

My first assumption was a network problem, so I verified that my internet connection was working correctly. It was.

What the Hell Is TLS and Why It Blocked Me

At this point, it’s useful to understand what’s actually failing here.

TLS stands for Transport Layer Security, which is what keeps data safe and encrypted when it travels over the internet. It ensures that when a machine connects to a server, the data can’t be read by or altered by anyone in between.

To determine whether this was a general TLS issue or something more specific, I tested another HTTPS endpoint:

curl -I https://google.com
Enter fullscreen mode Exit fullscreen mode

The TLS handshake completed successfully, confirming that HTTPS worked for major domains.

From the response, I could confirm:

TLS 1.3 handshake completed

Valid CA chain,

IPv4 + IPv6 both resolved,

HTTP/2 negotiated.
Enter fullscreen mode Exit fullscreen mode

So this ruled out a general network failure and pointed to selective TLS interference.

Why Google Works but Tailscale Fails

Here’s the thing.

Some networks allow traffic to popular CDNs like Google, while silently blocking or interfering with less common endpoints. This is common with mobile broadband, carrier-grade NATs, and Nigerian ISPs.

So the fact that google.com worked didn’t mean everything was fine. It only meant Google was allowed through.

The Pattern and The Hypothesis:

At this point, the pattern was clear. Connections worked for major CDNs but failed for certain domains. This usually indicates interference somewhere along the network path. So I needed a way to change the network path entirely.

So a question came up in my head:
What if I changed the network path entirely? What would happen?
Oops! Let’s make that two questions.

Enter the VPN: Just Testing:

To change the network path, I used a VPN [Virtual Private Network]. A VPN is a simple way to route traffic through a different geographic and network endpoint. At this stage, I wasn’t “solving” anything yet; I was testing a hypothesis.
And if things worked on VPN, then the network is blocking Tailscale.

I used Proton VPN, which I already had installed on my system.

All I had to do was open the Proton VPN app and log in. I logged in using my email and password. I switched to a server in another country and then ran:

curl ifconfig.me
Enter fullscreen mode Exit fullscreen mode

I ran this command before and after switching servers.

The output changed, confirming that my traffic was now coming from a different public IP and location.

That was all I needed.

Installing Tailscale Adding the Repository

So after changing location, I fired this command in the terminal again:

curl -fsSL https://tailscale.com/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

It succeeded this time around. wheeeeew!!! And here’s what happened:

The script added Tailscale’s official repository for my Ubuntu version (noble).
It imported the signing key.
Ran apt-get update and fetched package lists.
Installed Tailscale packages from https://pkgs.tailscale.com/stable/ubuntu noble/main
Enter fullscreen mode Exit fullscreen mode

No TLS errors. No handshake failures.

At that point, it was clear:

Changing the network path bypassed the interference.

So, it's game on…

Installing Tailscale: The Real Deal:

With the repository in place, I installed Tailscale using the system package manager:

sudo apt-get install tailscale
Enter fullscreen mode Exit fullscreen mode

Running that command prompted my system for a Unix user password, not a Tailscale password, not an email password. But the same password I use to log into my Linux account.
This is required because installing system packages requires root access.

After entering the password and confirming with y, the system will:

  • Download the Tailscale package

  • Install the tailscale CLI

  • Install the tailscaled background service

  • Register the service with systemd

  • Enable it to start automatically

Bringing the Device Online:

After installation, I ran this command:

 sudo tailscale up
Enter fullscreen mode Exit fullscreen mode

This command authenticates the device and joins it to my private Tailscale network.

Immediately, the terminal printed:

To authenticate, visit:

        https://login.tailscale.com/a/1***

Enter fullscreen mode Exit fullscreen mode

I clicked the link and was taken to a sign-in page.

I signed in using my Google account.

Device Authorization and Onboarding

After signing in, I was prompted to connect the device.

Clicked on the connect button, and I saw success in the terminal.

Success
Enter fullscreen mode Exit fullscreen mode

That success message was the green light; the device was now authenticated.

I was then redirected to a short onboarding screen with a form.

So I filled out Tailscale’s onboarding form.

I shared my primary reason for using Tailscale (Application Testing),
My role (Engineer),
and how I heard about Tailscale (YouTube).

Once complete, my desktop was added to my Tailscale network, and I was shown how to add additional devices across Windows, macOS, Linux, and mobile platforms.

Adding My Windows Machine:

On the official downloads page, the Windows section provides a direct link to download the Tailscale installer for Windows 10 or later.

Link here:

https://tailscale.com/download/windows
Enter fullscreen mode Exit fullscreen mode

Downloaded it and ran it.

After running the installer, I got this:

This shows that our installation was successful on Windows. Then I clicked on Get started to log in or sign in. Signed in with my Google account again, and was redirected to my dashboard.

After signing in, we see:

Two Machines, One Private Network:

At this point, we are fully connected to TailScale. Here’s what it means:

I have 2 machines connected to the same tailnet:

desktop-v2***** (Linux / WSL2)
    IP: 100.69.232.125
    Status: Connected
    This is my Ubuntu / WSL environment

desktop-v2***** (Windows 11)
    IP: 100.84.202.98
    Status: Connected
    This is my Windows environment
Enter fullscreen mode Exit fullscreen mode

What this means is:

  • Tailscale is installed correctly.

  • Authentication worked

  • The Nigerian ISP + VPN issues are completely bypassed.

What Is a Tailnet?

O.k. It will be best for me to talk about tailnet, because I mentioned it here:

I have 2 machines connected to the same tailnet:
Enter fullscreen mode Exit fullscreen mode

So, Tailnet is Tailscale’s private network. It’s a collection of devices that you have authenticated using Tailscale. Once a device joins the tailnet, it gets a private IP address and can communicate with other devices on the same network. No public internet is required.

Now that we have trashed that, let’s move on…

Checking the Tailscale Status:

So to see if Tailscale is really working on our machine, I ran this in the terminal:

tailscale status
Enter fullscreen mode Exit fullscreen mode

And I got this response:

100.69.232.125  desktop-v2*****    v@  linux    -  
100.84.202.98   desktop-v2*****  v@  windows  -  
Enter fullscreen mode Exit fullscreen mode

Hell Yeah! This confirmed that both machines were:

  • connected

  • authenticated

  • visible inside the same tailnet

Starting the Web App via Tailscale

Next up, I fired up the MERN application.

Inside my VS Code, I first moved to my home directory by running this:

 cd ~
Enter fullscreen mode Exit fullscreen mode

Then I opened the Linux workspace by typing:

code .
Enter fullscreen mode Exit fullscreen mode

From there, I navigated to the deployment directory:

cd deploy
Enter fullscreen mode Exit fullscreen mode

Then I started up the Docker containers:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Mind you, my Docker Desktop application was already open with the engine running.

The response from Docker was:

[+] Running 7/7
 ✔ Network deploy_default            Created                                                                                                                    0.0s 
 ✔ Container deploy-redis-1          Started                                                                                                                    1.3s 
 ✔ Container deploy-mongodb-1        Started                                                                                                                    1.3s 
 ✔ Container deploy-server3-1        Started                                                                                                                    1.7s 
 ✔ Container deploy-server1-1        Started                                                                                                                    1.8s 
 ✔ Container deploy-server2-1        Started                                                                                                                    1.7s 
 ✔ Container deploy-reverse-proxy-1  Started    
Enter fullscreen mode Exit fullscreen mode

Then I checked to see if all containers were up and running:

docker ps
Enter fullscreen mode Exit fullscreen mode

Got this as an output:

CONTAINER ID   IMAGE                         COMMAND                  CREATED          STATUS          PORTS                NAMES
5006e66c1b5b   vawkei/reverse-proxy:latest   "/docker-entrypoint.…"   45 seconds ago   Up 42 seconds   0.0.0.0:80->80/tcp   deploy-reverse-proxy-1
b976608a5480   vawkei/server2:latest         "docker-entrypoint.s…"   45 seconds ago   Up 43 seconds   5000/tcp             deploy-server2-1
4f1663befaea   vawkei/server3:latest         "docker-entrypoint.s…"   45 seconds ago   Up 43 seconds   5000/tcp             deploy-server3-1
fdd6aa247231   vawkei/server1:latest         "docker-entrypoint.s…"   45 seconds ago   Up 43 seconds   5000/tcp             deploy-server1-1
a3f25b1ba739   mongo:8.2                     "docker-entrypoint.s…"   46 seconds ago   Up 44 seconds   27017/tcp            deploy-mongodb-1
6f18d754bba5   redis:6                       "docker-entrypoint.s…"   46 seconds ago   Up 44 seconds   6379/tcp             deploy-redis-1
Enter fullscreen mode Exit fullscreen mode

BRAAAAA!!!
This showed that all containers were working.

Accessing the App via Tailscale:

So at this moment, I was sitting on top of the world like Mase and Brandy in 1998, I tried accessing the web app on this URL:

http://100.69.232.125/
Enter fullscreen mode Exit fullscreen mode

I was all smiles and about popping the champagne bottle, lo and behold, I got hit with this response from my web page.

Your Internet access is blocked
ERR_NETWORK_ACCESS_DENIED
Enter fullscreen mode Exit fullscreen mode

I froze like I got hit with an Ice Ball from Sub-Zero in Mortal Kombat. I was like, whaaaat! It ain't possible. What the hell is this?

This app was literally running. Containers were up. Tailscale was connected. So it wasn't adding up.

Calmed down, took a breather, and walked away to clear my head.

After some hours of thought, I came back to my laptop and tried this instead:

http://100.84.202.98/
Enter fullscreen mode Exit fullscreen mode

Hell Yeah! It worked. I was glad and gave an evil grin.

The web app opened in the browser and was accessible.

So What the Hell Happened?

Let me explain:

I had two Tailscale IPs:

100.69.232.125  desktop-v2*****    v@  linux    -  
100.84.202.98   desktop-v2*****  v@  windows  -  
Enter fullscreen mode Exit fullscreen mode

So when I tried accessing:

http://100.69.232.125/
Enter fullscreen mode Exit fullscreen mode

From Windows, the request path looked like this:

Windows → Tailscale → WSL2 → Docker → Nginx

Windows blocked it before it ever reached Docker.

This wasn’t a Tailscale failure.

This was Windows Defender / firewall guarding cross-interface traffic. which is a common Windows + WSL2 behavior.

Why the Second IP Worked:

When I accessed:

http://100.84.202.98/
Enter fullscreen mode Exit fullscreen mode

I was hitting Windows’ own Tailscale interface.

The App loaded, and the Firewall allowed it.

That worked because:

I was accessing Windows’ own Tailscale interface, and Windows already trusts itself.
This led Traffic to be forwarded internally to WSL2, where Nginx was listening on port 80.

The Not Secure Warning in the Browser

In the picture, you can see Not Secure up there. Even though the browser shows “Not Secure”, all traffic is still encrypted by Tailscale. The warning appears only because the app is served over HTTP and does not use a public TLS certificate. So it’s all good.

Final Notes Scope and Access

One important clarification before wrapping up.
The Tailscale IP used here:

100.84.202.98
Enter fullscreen mode Exit fullscreen mode

It is not a public IP.

It’s a private IP address that exists only inside my tailnet. This means:

  • It’s not reachable from the public internet
  • It won’t load on random mobile data
  • It won’t work without authentication

Only devices that:

  • Have Tailscale installed
  • Are logged into the same tailnet
  • And are actively connected

Can access the app.

And that’s the whole point of this.

This setup was never meant to expose the app publicly. It was meant to securely expose a local, Dockerized MERN app for testing, development, and internal access without domains, port forwarding, or cloud infrastructure.

Thanks for Reading.

I will see you in the next ONE.

Top comments (0)