Hi everyone,
I'd like to share a method I worked out for running Tailscale inside an Enhance CP website container - with no root, no package install, and zero risk to the container. This lets you reach private services (a database on another host/VPC, an internal API, Redis, etc.) securely over Tailscale, without opening public ports or maintaining IP whitelists. I couldn't find a complete guide for doing this inside a managed Enhance container, so here it is.
What this enables: Any device on your Tailscale network can reach services on the container's private network by their private IPs, from anywhere, with no public exposure. In my case I use it as a gateway to reach production databases on a private VPC - the office-IP-whitelist recurring task is gone.
Why the normal Tailscale install does NOT work in an Enhance container
The default install wants root, creates a TUN network interface, writes iptables/nftables rules, and rewrites /etc/resolv.conf. In a managed Enhance website container you don't have root, and those changes would fight the container. The fix is Tailscale's userspace-networking mode: it runs as a plain unprivileged process in your home directory - no TUN device, no firewall rules, no DNS changes, no system files touched. Its entire footprint is one folder you can delete.
Trade-off: userspace mode forwards TCP/UDP only (no ICMP ping through the routes - that's fine for MySQL/Redis/SSH/HTTP). Throughput is userspace-speed, which is more than enough for database and API traffic.
Step 1: Enable SSH for the site and connect
In Enhance CP, enable SSH/SFTP for the website, add your key, and connect as the site's SSH user (NOT root):
ssh {site_ssh_user}@{server_ip}
# Confirm your home dir, e.g. /var/www/{UUID}
echo $HOME
Step 2: Download the static Tailscale binaries into your home directory
Grab the latest amd64 static tarball (check https://pkgs.tailscale.com/stable/ for the current version):
mkdir -p ~/tailscale/state
cd ~/tailscale
curl -fsSL -o ts.tgz https://pkgs.tailscale.com/stable/tailscale_1.98.8_amd64.tgz
tar xzf ts.tgz
cp tailscale_1.98.8_amd64/tailscaled ~/tailscale/
cp tailscale_1.98.8_amd64/tailscale ~/tailscale/
~/tailscale/tailscaled --version # sanity check it runs
Step 3: Start the daemon in userspace mode
Note the flags: userspace networking, no DNS changes, and a state file + control socket kept inside your home dir (so nothing needs /var/run or root):
nohup ~/tailscale/tailscaled \
--tun=userspace-networking \
--state=$HOME/tailscale/state/tailscaled.state \
--socket=$HOME/tailscale/ts.sock \
>> $HOME/tailscale/tailscaled.log 2>&1 &
Step 4: Bring it up and advertise the routes you want to reach
Advertise the private IPs your other devices should be able to reach (use narrow /32 host routes so you don't collide with home/office LANs). Omit --advertise-routes entirely if you only want this box itself on the network.
~/tailscale/tailscale --socket=$HOME/tailscale/ts.sock up \
--accept-dns=false \
--advertise-routes={TARGET_PRIVATE_IP_1}/32,{TARGET_PRIVATE_IP_2}/32 \
--hostname=my-gateway
This prints a login URL. Open it, sign in to your tailnet, and authorize the node.
Step 5: Approve the routes in the admin console
Advertised routes do nothing until you approve them: Tailscale admin console → Machines → your node → Edit route settings → approve each route. Also disable key expiry for this node, or it will silently drop off the network in 180 days.
Keeping it alive - the Enhance cron gotcha
A userspace daemon started with nohup won't survive a container reboot, and here's the catch: the crontab CLI is blocked inside Enhance website containers (you'll see Command unavailable in website container), and there's no systemd user session either. So you can't schedule a keep-alive from the shell.
The fix is to use Enhance CP → the website → Cron Jobs (the panel scheduler) to run a small watchdog script. Create the watchdog first (note it uses absolute paths - the panel cron runs with no $HOME):
# ~/tailscale/watchdog.sh (chmod +x it)
#!/bin/bash
TSDIR=/var/www/{UUID}/tailscale
pgrep -f 'tailscaled --tun=userspace-networking' >/dev/null && exit 0
nohup "$TSDIR/tailscaled" --tun=userspace-networking \
--state="$TSDIR/state/tailscaled.state" \
--socket="$TSDIR/ts.sock" >> "$TSDIR/tailscaled.log" 2>&1 &
Then add a Cron Job in the panel:
# Schedule: every 1 minute
* * * * * /var/www/{UUID}/tailscale/watchdog.sh
This covers both a daemon crash and a container reboot - the node is back within a minute, and because the routes are saved in the state file, it reconnects with no re-login needed.
Gotcha: what IP your target service sees
If the service you're reaching does its own IP allow-listing (e.g. a MariaDB with a remote-access whitelist), it will see connections arriving from the container's internal bridge IP (typically 10.169.0.x), NOT the host's public or VPC IP. To find the exact IP to whitelist, trigger a rejected connection and read it from the error, e.g.:
mysqlsh --sql -e "SELECT 1" "mysql://user:wrongpass@{target_ip}:3306"
# -> Host '10.169.0.x' is not allowed to connect to this MariaDB server
Whitelist that 10.169.0.x address (or the 10.169.0.0/24 range). Note it can change if the container is re-created.
Uninstall: leaves the container byte-identical
pkill -f 'tailscaled --tun=userspace-networking'
rm -rf ~/tailscale
# then delete the Cron Job in the panel
Security note: lock down who can reach the services with Tailscale ACLs, expose only the specific /32 host routes needed (never a whole subnet), and keep least-privilege credentials on the services themselves. The tunnel replaces public exposure, which is the main intention of this whole exercise.
Hope this saves someone the hours it took me to piece together. 🙂