Run Tailscale on Upsun: Create secure VPN connections for your applications

Run Tailscale on Upsun: Create secure VPN connections for your applications

September 16, 2025· Florian Margaine
Florian Margaine
·Reading time: 4 minutes

Tailscale is a VPN service that creates secure, private networks between your devices and applications. By integrating Tailscale with your Upsun applications, you can make your containers part of a “tailnet” - Tailscale’s term for their private networks.

This guide demonstrates how you can leverage Tailscale to connect to external endpoints over a secure network. An example Go application will be added as a placeholder and example for your own application. This setup enables secure communication between your application and other resources in your tailnet.

Prerequisites

Before starting, you’ll need:

  • A Tailscale account with an auth key
  • Basic familiarity with Upsun configuration
  • Understanding of Go applications

Tailscale implementation limitations

Tailscale usually relies on a specific tunnel networking device (tun0) to route traffic from their machine to their network. Because Upsun run containers directly, a userspace application can’t add a new network interface. To work around this limitation, Tailscale can be started as a HTTP proxy to the tailnet. Our example application will connect to the external endpoints via this exposed HTTP proxy.

Create a basic Go application

Start by creating a simple Go application. Create a go.mod file:

go.mod
module example-app
go 1.25

Create a main.go file:

main.go
package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, Upsun!"))
	})
	err := http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil)
	panic(err)
}

Create your initial Upsun configuration in .upsun/config.yaml:

.upsun/config.yaml
routes:
  https://{default}:
    type: upstream
    upstream: app:http

applications:
  app:
    type: golang:1.25
    web:
      commands:
        start: ./hello
      locations:
        '/':
          passthru: true
    hooks:
      build: go build -o hello

This basic configuration creates a working example Go application on Upsun. You now need to add the Tailscale integration to the project.

Configure multiple processes with Supervisor

Since Upsun’s web.commands.start is designed to run only one process, you will use Supervisor to start and manage both your application and the Tailscale daemon processes. Update your .upsun/config.yaml:

.upsun/config.yaml
routes:
  https://{default}:
    type: upstream
    upstream: app:http

applications:
  app:
    type: golang:1.25
    dependencies:
      python3:
        supervisor: '*'
    web:
      commands:
        start: supervisord -n -c supervisor.conf
      locations:
        '/':
          passthru: true
    hooks:
      build: go build -o hello

Create a supervisor.conf file to manage your processes:

supervisor.conf
[supervisord]
logfile=/tmp/supervisord.log
nodaemon=true
minfds=1024
pidfile=/tmp/supervisord.pid

[program:hello]
command=/app/hello
process_name=%(program_name)s
autostart=true
autorestart=true

Download and configure Tailscale

The easiest way to run Tailscale is to download and run the amd64 binary directly. Create a script to download Tailscale and cache it between builds. Create scripts/download-tailscale.sh:

scripts/download-tailscale.sh
#!/bin/bash

set -e

version=1.86.2

# This uses $PLATFORM_CACHE_DIR to avoid re-downloading the binary on every build
cache="$PLATFORM_CACHE_DIR"/tailscale_"$version"_amd64.tgz

if [ ! -f "$cache" ]; then
    curl https://pkgs.tailscale.com/stable/tailscale_"$version"_amd64.tgz -o "$cache"
fi

tar xf "$cache"
mv tailscale_"$version"_amd64 tailscale

Make the script executable:

Terminal
chmod +x scripts/download-tailscale.sh

Update your build hook in .upsun/config.yaml to use this script:

.upsun/config.yaml
applications:
  app:
    # ... rest of configuration ...
    hooks:
      build: |
        set -e
        ./scripts/download-tailscale.sh
        go build -o hello        

Add Tailscale daemon to Supervisor

Update your supervisor.conf file to include the Tailscale daemon:

supervisor.conf
[supervisord]
logfile=/tmp/supervisord.log
nodaemon=true
minfds=1024
pidfile=/tmp/supervisord.pid

[program:hello]
command=/app/hello
process_name=%(program_name)s
autostart=true
autorestart=true

[program:tailscale]
command=/app/tailscale/tailscaled --state=mem: --socket=/tmp/tailscaled.sock --tun=userspace-networking --outbound-http-proxy-listen=0.0.0.0:8080 --socks5-server=0.0.0.0:1080
process_name=%(program_name)s
autostart=true
autorestart=true

Key configuration options:

  • --state=mem: creates a new Tailscale client on each restart. For persistent clients, use a file on a mount instead
  • --outbound-http-proxy-listen=0.0.0.0:8080 exposes the VPN through an HTTP proxy on port 8080
  • --socks5-server=0.0.0.0:1080 provides SOCKS5 proxy access on port 1080

Connect to your tailnet

After the Tailscale daemon starts, you need to run tailscale up to join your tailnet. You could add this to your post_start command:

.upsun/config.yaml
applications:
  app:
    # ... rest of configuration ...
    web:
      commands:
        start: supervisord -n -c supervisor.conf
        post_start: /app/tailscale/tailscale --socket="$sock" up --auth-key="$TAILSCALE_AUTHKEY"

However, the tailscaled daemon needs time to start, causing tailscale up to fail. Instead, create a scripts/tailscale-up.sh script that waits for the daemon:

scripts/tailscale-up.sh
#!/bin/bash

set -xe

sock=/tmp/tailscaled.sock

while ! curl --unix-socket "$sock" localhost -w1; do
    sleep 0.5
done

/app/tailscale/tailscale --socket="$sock" up --auth-key="$TAILSCALE_AUTHKEY"

Make this script executable:

chmod +x scripts/tailscale-up.sh

And update your Upsun configuration to use this script:

.upsun/config.yaml
applications:
  app:
    # ... rest of configuration ...
    web:
      commands:
        start: supervisord -n -c supervisor.conf
        post_start: ./scripts/tailscale-up.sh

Set up authentication

Create an environment variable with your Tailscale authentication key:

upsun variable:create env:TAILSCALE_AUTHKEY your-auth-key-here

Your application container is now part of your tailnet!

Using your tailnet connection

Outbound connections

To connect to resources in your tailnet from your application, use the HTTP proxy on port 8080:

# Example with curl
http_proxy=localhost:8080 curl 100.87.175.78

As mentioned earlier, our applications needs to configure their HTTP proxy settings to be able to connect to other machines on the tailnet. Refer to the proxy documentation of your specific runtime or framework.

Inbound connections

Your application becomes immediately accessible from other devices in your tailnet. To expose additional services like databases, use tools like socat to forward traffic:

# Forward database traffic (example)
socat tcp-listen:3306,bind=0.0.0.0,fork,reuseaddr tcp:database.internal:3306

Next steps

With Tailscale running on Upsun, you can:

  • Connect securely to your application from any device in your tailnet
  • Access private databases and services through your application
  • Create secure communication channels between multiple Upsun applications
  • Implement zero-trust networking for your infrastructure

Get started with Upsun and create your secure, scalable application infrastructure today.

Last updated on