Skip to content

๐ŸŒ Exposing Securely

Exposing self-hosted services securely on the internet is one of the most common challenges. This guide explains how to make LibreFolio (or any other service in your local network) accessible by leveraging Tailscale, a secure, high-performance, and free mesh VPN solution for home use.

Our Configuration Recommendation

Among the different approaches presented, we believe that Level 4 (Multi-Funnel via Docker) is the absolute best solution: it requires very little additional configuration compared to the other methods, offers the maximum advantages in terms of isolation and modularity, and resolves the structural limitations of the other methods. The other levels are presented both as alternatives and to understand the technical path to get there.


๐Ÿ”’ Security and Risks of Traditional Port Forwarding

The traditional method for making a service accessible from the outside involves opening ports on your home router (port forwarding) associated with a public IP (often dynamic) and a DDNS service (like DuckDNS).

This approach presents significant risks:

  1. Exposure to the entire web: Anyone can scan your public IP and attempt to attack the open port.
  2. Management complexity: It is necessary to manually configure and renew SSL certificates (HTTPS) via a reverse proxy (Nginx, Caddy, etc.).
  3. HTTP protocol risks: Without a correctly configured HTTPS encryption, your credentials and financial data travel in plain text over the local and public network, making them interceptable by malicious actors (packet sniffing).

The following diagram shows the initial remote exposure problem:

graph LR
    User["๐Ÿ‘ค External User<br>(Away from Home)"] --- Cloud["โ˜๏ธ Internet / Router (Public IP / DDNS?)"]
    Cloud --- Server["๐Ÿ–ฅ๏ธ Local Server<br>(Port 6040)"]

๐Ÿš€ What is Tailscale?

Tailscale is a zero-configuration mesh VPN service based on the modern WireGuard encryption protocol.

  • Free Plan (Personal): Allows connecting up to 100 devices for free.
  • Mesh Network: All configured devices connect directly to each other in an encrypted peer-to-peer fashion, without traffic passing through intermediate servers.
  • Compatibility: Works on all major operating systems (Linux, macOS, Windows, iOS, Android) and can be installed on a NAS or inside Docker containers.

๐Ÿ Step 0: Installing Tailscale on Your Devices

To make any VPN work, at least 2 connected devices are required: the client (e.g., your smartphone or laptop) and the server (the node on which LibreFolio is running). Before proceeding with the levels, install and log into Tailscale on your devices:

Run the official installation command on the server:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

For more details, see the Generic Installation Guide.

Install the official app from the Mac App Store or use Homebrew:

brew install --cask tailscale
sudo tailscale up

For more details, see the Generic Installation Guide.

Download the official installer from the Tailscale portal and follow the login wizard.

For details, see the Windows Installation Guide.

Install the official application from the Google Play Store.

Install the official application from the Apple App Store.


๐Ÿ› ๏ธ The 4 Levels of Configuration and Exposure


๐Ÿƒ Level 1: Private Point-to-Point VPN Connection (Start)

This consists of connecting the server and the client to the same private Tailscale network. On the server, the service port is exposed using the serve command.

graph LR
    Client["๐Ÿ‘ค Client (VPN active)<br>(100.x.y.z)"] -->|Direct VPN Connection| Server["๐Ÿ–ฅ๏ธ Server (VPN active)<br>(100.a.b.c:6040)"]
    subgraph LAN ["Local LAN Network"]
        Server -->|Local access| LibreFolio["๐Ÿ“Š LibreFolio (Local)"]
    end
    style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;

On the server, use the command to expose the local LibreFolio port (default port 6040):

tailscale serve tcp:6040 /

At this point, with the VPN active on your smartphone or PC, simply enter the server's Tailscale IP (or its MagicDNS) followed by the port in the browser to access LibreFolio remotely.

๐ŸŸข Advantages (Pros) ๐Ÿ”ด Disadvantages (Cons)
  • Instant and minimal configuration.
  • Maximum security: your data does not pass over the public internet, the port is closed outside the VPN.
  • Requires the Tailscale VPN to be active and connected on each client (e.g., on the phone) to reach the service.
  • Exposes only one single service per host.

๐Ÿฅ‰ Level 2: Subnet Router Configuration (LAN Tunneling)

This level transforms your server into a "sub-router". When you are away from home with the VPN turned on on the client, you can reach not only the server but any device or service on your home LAN by simply entering its local IP.

graph LR
    Client["๐Ÿ‘ค Client (VPN active)<br>(100.x.y.z)"] -->|WireGuard Tunneling| Server["๐Ÿ–ฅ๏ธ Server (Subnet Router)<br>(100.a.b.c)"]
    subgraph LAN ["Local LAN Network (192.168.1.0/24)"]
        Server -->|Local forwarding| LibreFolio["๐Ÿ“Š LibreFolio<br>(e.g. 192.168.1.2:6040)"]
        Server -->|Local forwarding| OtherDevice["๐Ÿ–จ๏ธ Other Devices/Services<br>(e.g. 192.168.1.100)"]
    end
    style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;

1. Enable Subnet Routing on the Server OS

Enable IP forwarding at the kernel level:

echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

Start advertising the subnet (replace the IP range with your local network, e.g., 192.168.1.0/24):

sudo tailscale up --advertise-routes=192.168.1.0/24

Use the Tailscale executable path to advertise the local subnet:

/Applications/Tailscale.app/Contents/MacOS/Tailscale up --advertise-routes=192.168.1.0/24

Run Command Prompt (cmd.exe) or PowerShell as Administrator and advertise the local subnet:

tailscale up --advertise-routes=192.168.1.0/24

2. Approve the Route in the Admin Console

  1. Go to the Tailscale Admin Console.
  2. Click the three dots next to your server -> Edit route settings.
  3. Enable the advertised subnet.

Disable Key Expiry for the Server

Since the server acts as network infrastructure (subnet router), it is recommended to disable automatic key expiry for this node to prevent it from disconnecting and requiring periodic interactive reauthentication (every 180 days by default):

  1. On the Machines page of the admin console, locate your server.
  2. Click the three dots (...) icon on the right of the device row.
  3. Select the Disable Key Expiry option.
๐ŸŸข Advantages (Pros) ๐Ÿ”ด Disadvantages (Cons)
  • Access to all devices in the house (printers, cameras, LibreFolio, home automation) with only one active node.
  • No need to configure ports or reverse proxies for each service.
  • The VPN on the client must be active to allow communication.
  • You must know the local IPs of the devices to reach them.
  • Once inside the home, packets travel in plain text (HTTP) on the private LAN.

๐Ÿ”‘ Enabling Funnel and ACLs on the Console

One-time configuration required for Level 3 and Level 4

Before you can use Tailscale Funnel (either on the local server in Level 3 or inside Docker containers in Level 4), you must enable Funnel and define the global access control rules (ACLs) for your entire Tailnet. This is a one-time setup performed directly in the Tailscale admin console.

1. Enable HTTPS and Funnel on the Control Panel

  1. Visit the Access Controls page in the Tailscale admin console.
  2. Click the Add node attribute button to create the required authorization.

Add Node Attribute

  1. Configure the following options in the form:
    • Targets: Enter the tag or group you want to authorize for Funnel activation. A Target defines which nodes the rule applies to. We suggest using tag:external_access (to selectively associate it with Docker containers) or autogroup:member (if you want to allow exposure for all devices registered under your personal account).
    • Attributes: Enter funnel.
    • Note: Enter some text to record the reason for this rule.
    • IP Pools, App, Capability, etc.: These extra fields are not needed for this exposure setup, so leave them empty or at their default values.

Important: ACL configuration defines the global security policies required to enable Funnel. It is independent from authentication keys (Auth Keys), which are used only to register a new device or container on the network for the first time.

Alternatively, if you prefer to edit the ACL JSON configuration directly, you can use the following working example (updated to support both your own devices and the containers tagged with tag:external_access):

View the complete ACL JSON configuration to enable Funnel
{
  // Declaration of authorized tags
  "tagOwners": {
    "tag:external_access": ["autogroup:admin"]
  },

  // Standard access rules
  "acls": [
    // Allows all nodes in your private network to communicate
    {"action": "accept", "src": ["*"], "dst": ["*:*"]}
  ],

  "ssh": [
    {
      "action": "check",
      "src":    ["autogroup:member"],
      "dst":    ["autogroup:self"],
      "users":  ["autogroup:nonroot", "root"]
    }
  ],

  // Enabling Funnel on specific nodes or tags
  "nodeAttrs": [
    {
      "target": ["autogroup:member"],
      "attr":   ["funnel"]
    },
    {
      "target": ["tag:external_access"],
      "attr":   ["funnel"]
    }
  ]
}

๐Ÿฅˆ Level 3: Public Exposure via Tailscale Funnel (No VPN on Client)

Fundamental Prerequisite

Before proceeding, make sure you have completed the one-time Funnel and ACL configuration on the console.

Tailscale Funnel lets you expose a service publicly on the internet. Anyone can access your LibreFolio instance through a secure HTTPS URL provided by MagicDNS, without needing to install or activate Tailscale on their smartphone or PC. This is essential if you want to install LibreFolio as a PWA on mobile devices and get the automatic install prompt (for more details, see the guide ๐Ÿ“ฑ Install as App (PWA)).

graph LR
    User["๐Ÿ‘ค User (No VPN)"] -->|HTTPS Request| Funnel["โ˜๏ธ Tailscale Funnel Ingress<br>(Tailscale Public Server)"]
    Funnel -->|WireGuard Tunneling| Server["๐Ÿ–ฅ๏ธ Local Server (tailscaled)<br>(100.a.b.c)"]
    subgraph LAN ["Local LAN Network"]
        Server -->|Local forwarding| LibreFolio["๐Ÿ“Š LibreFolio (Port 6040)"]
        Server -.->|"<font color='red'><b>Cannot expose</b></font>"| Other["๐Ÿ”Œ Other Local Services (Different ports)"]
    end
    style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;
    style Other fill:#f8d7da,stroke:#dc3545,stroke-width:2px;
    linkStyle 3 stroke:#dc3545,stroke-width:2px;

1. Start the Funnel on the Server

Associate the funnel with the local LibreFolio port:

tailscale funnel 6040 on

Note: For this level, no authentication key (Auth Key) is required as the server machine has already been logged in and registered interactively to your Tailnet during Step 0.

2. Approve and Wait for Propagation

Once the command is launched, a warning will appear in the terminal indicating that the Funnel is enabled but not yet authorized for your node, showing a link similar to the following:

Funnel is enabled, but the list of allowed nodes in the tailnet policy file does not include the one you are using.
To give access to this node you can edit the tailnet policy file, or visit:

         https://login.tailscale.com/f/funnel?node=xxxxxx
  • Visit the link shown in the browser, log in to Tailscale, and approve the activation of the Funnel for this node.
  • Once approved, the terminal will display the generated public URL.
  • Wait a few minutes for the MagicDNS records to propagate globally to reach the service from any external network.
๐ŸŸข Advantages (Pros) ๐Ÿ”ด Disadvantages (Cons)
  • Universal public access via free HTTPS managed by Tailscale.
  • No SSL certificate or reverse proxy to configure on the server.
  • Allows native PWA installation on smartphones without turning on the VPN.
  • You can expose at most 1 single service Funnel per host machine.

๐Ÿฅ‡ Level 4: Advanced Multi-Funnel Exposure via Docker (Sidecars)

Fundamental Prerequisite

Before proceeding with the container configuration, make sure you have completed the one-time Funnel and ACL configuration on the console.

To overcome the limit of one Funnel per host node, we can run multiple parallel Tailscale nodes inside Docker containers. Each container will register as an independent node on your Tailnet, obtaining its own dedicated MagicDNS URL.

Our solution uses a small custom startup script that installs socat in the container and redirects incoming HTTPS traffic to the static LAN IP of the target service.

What is socat?

socat (SOcket CAT) is an extremely flexible command-line utility that establishes two bidirectional byte streams and transfers data between them. In our case, we use it as a mini proxy-forwarder: it listens on the local port of the Tailscale container and forwards all received packets to the real port of the service on the local server.

The network diagram illustrates the multi-node scenario exposed in parallel, where Tailscale containers 1 and 2 run on the first host (Server 1) and Tailscale containers 3 and 4 run on the second host (Server 2):

graph LR
    User["๐Ÿ‘ค External User"] -->|HTTPS| Funnel1["โ˜๏ธ Funnel 1<br>(service2.yourtailnet.ts.net)"]
    User -->|HTTPS| Funnel2["โ˜๏ธ Funnel 2<br>(service3.yourtailnet.ts.net)"]
    User -->|HTTPS| Funnel3["โ˜๏ธ Funnel 3<br>(librefolio.yourtailnet.ts.net)"]
    User -->|HTTPS| Funnel4["โ˜๏ธ Funnel 4<br>(service1.yourtailnet.ts.net)"]

    Funnel1 -->|WireGuard| TSC1["๐Ÿณ Tailscale Container 1<br>(100.1.1.1)"]
    Funnel2 -->|WireGuard| TSC2["๐Ÿณ Tailscale Container 2<br>(100.4.4.4)"]
    Funnel3 -->|WireGuard| TSC3["๐Ÿณ Tailscale Container 3<br>(100.3.3.3)"]
    Funnel4 -->|WireGuard| TSC4["๐Ÿณ Tailscale Container 4<br>(100.2.2.2)"]

    subgraph LAN ["Local LAN Network (192.168.1.0/24)"]
        subgraph Host2 ["Server 2 (e.g. Mini PC - 192.168.1.10)"]
            TSC3 -->|socat: TCP/8080| Service3["๐Ÿ”Œ Service 3<br>(192.168.1.10:80)"]
            TSC4 -->|socat: TCP/9000| Service4["๐Ÿ”Œ Service 4<br>(192.168.1.10:80)"]
        end
        subgraph Host1 ["Server 1 (e.g. NAS - 192.168.1.20)"]
            TSC1 -->|socat: TCP/6040| LibreFolio["๐Ÿ“Š LibreFolio<br>(192.168.1.20:6040)"]
            TSC2 -->|socat: TCP/80| Service1["๐Ÿ”Œ Service 1<br>(192.168.1.20:80)"]
        end
    end
    style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;

Multiple Nodes and Services

With this architecture, you can add and expose all desired services simply by starting new Tailscale containers associated with the relevant script. The only limit is set by the terms of your Tailscale subscription plan (which covers up to 100 devices in the free version).

1. Folder and Script Preparation

Create a folder on the server (e.g., inside the path where you keep your Docker persistent volumes):

# Create a folder for the Tailscale nodes and enter it
mkdir -p <path_chosen>/tailscale-nodes
cd <path_chosen>/tailscale-nodes

Download the custom startup script custom_startup.sh inside this folder:

# Download the script from the official repository
wget https://raw.githubusercontent.com/Librefolio/LibreFolio/main/docs/static/tailscale-guide/custom_startup.sh
# Make the script executable
chmod +x custom_startup.sh

2. Docker Compose Configuration

We suggest defining and declaring the Tailscale service within the same docker-compose.yml file as the service you want to expose (e.g., LibreFolio) to keep them close and logically coupled. Add the service block as shown below:

services:
  tailscale-librefolio:
    image: tailscale/tailscale:latest
    container_name: tailscale-librefolio
    hostname: tailscale-librefolio
    restart: unless-stopped
    privileged: false
    network_mode: bridge
    cap_add:

      - NET_ADMIN
      - NET_RAW
    devices:

      - /dev/net/tun:/dev/net/tun
    command:

      - /custom_startup.sh
    environment:

      - HOST_IP=192.168.1.10                # Local IP of the service to expose (e.g. Server 1)
      - HOST_PORT=6040                      # Real port of the service to expose
      - TAILSCALE_FUNNEL_PORT=6040          # Internal Funnel port
      - TS_HOSTNAME=librefolio              # Custom public hostname (e.g. librefolio)
      - TS_AUTHKEY=tskey-auth-...           # Authentication key generated by Tailscale
      - TS_ACCEPT_DNS=true
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_USERSPACE=false
    volumes:

      - <path_chosen>/tailscale-nodes/tailscale-librefolio/state:/var/lib/tailscale
      - <path_chosen>/tailscale-nodes/custom_startup.sh:/custom_startup.sh
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

Configuration Parameters Description

Parameter Description
<path_chosen> The absolute path (full-path) on the local server where the script and state data are saved (e.g. /home/user/docker).
HOST_IP The static LAN IP of the machine hosting the service.
HOST_PORT The real port on the LAN server to connect to (e.g. 6040 for LibreFolio).
TAILSCALE_FUNNEL_PORT The port on which the Tailscale container will listen and activate the Funnel. In principle, the best approach is to set this parameter to the same value as the internal service port (HOST_PORT) for consistency; it is left as a separate parameter to support potential future special cases.
TS_HOSTNAME The custom hostname for the node. The generated public address will be https://TS_HOSTNAME.your-tailnet.ts.net.
TS_AUTHKEY The authentication key (Auth Key) generated by Tailscale. To obtain it:
1. Go to Tailscale Admin Settings Keys.
2. Under the Auth keys section (not under the API access tokens section), click the Generate auth key... button.
3. You must enable the Tags toggle to select the desired tag (e.g., tag:external_access). In the key description, enter a descriptive note to make it easily recognizable (e.g., docker-librefolio-funnel).
4. Click Generate and copy the generated key (e.g., tskey-auth-...).

Note: Once the container has successfully started, the one-time key is consumed and automatically disappears from the "Keys" list in the admin console, while the new registered device will appear in "Machines".
View the complete production Docker Compose file (LibreFolio + Tailscale)

Below is a real and complete example of a production docker-compose.yml file that runs the official LibreFolio production image alongside the Tailscale sidecar for automatic exposure:

# =============================================================================
# LibreFolio โ€” Production Docker Compose
# =============================================================================
# Optimized for end-users running the official pre-built image from GHCR.
# =============================================================================

services:
  librefolio:
    image: ghcr.io/librefolio/librefolio:nightly
    container_name: librefolio
    restart: unless-stopped
    ports:

      - "${PORT:-6040}:6040"
    volumes:

      - ./LibreFolio-data:/app/backend/data/prod-docker
    env_file: .env
    environment:

      - LIBREFOLIO_DATA_DIR=/app/backend/data/prod-docker
      - HOST=0.0.0.0
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:6040/api/v1/system/health')"]
      interval: 30s
      timeout: 10s
      start_period: 15s
      retries: 3

  tailscale-librefolio:
    image: tailscale/tailscale:latest
    container_name: tailscale-librefolio
    hostname: tailscale-librefolio
    restart: unless-stopped
    privileged: false
    network_mode: bridge
    cap_add:

      - NET_ADMIN
      - NET_RAW
    devices:

      - /dev/net/tun:/dev/net/tun
    command:

      - /custom_startup.sh
    environment:

      - HOST_IP=192.168.1.10                # Local IP of the service to expose (e.g. Server 1)
      - HOST_PORT=6040                      # Real port of the service to expose
      - TAILSCALE_FUNNEL_PORT=6040          # Internal Funnel port
      - TS_HOSTNAME=librefolio              # Custom public hostname (e.g. librefolio)
      - TS_AUTHKEY=tskey-auth-...           # Replace with your generated key
      - TS_ACCEPT_DNS=true
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_USERSPACE=false
    volumes:

      - /DATA/AppData/tailscale-nodes/tailscale-librefolio/state:/var/lib/tailscale
      - /DATA/AppData/tailscale-nodes/custom_startup.sh:/custom_startup.sh
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

3. Startup and Approval

Start the compose container of your service (inclusive of the Tailscale sidecar):

docker compose up -d

View the logs of the Tailscale container to extract the Funnel approval link (required on first startup):

docker logs -f tailscale-librefolio

In the container logs, a warning line will appear with the specific authorization link for your node:

Funnel is enabled, but the list of allowed nodes in the tailnet policy file does not include the one you are using.
To give access to this node you can edit the tailnet policy file, or visit:

         https://login.tailscale.com/f/funnel?node=nsKGo6k9ZF11CNTRL
  • Open the link shown in the browser, log in to Tailscale, and approve the Funnel activation.
  • Immediately after approval, you will see confirmation of successful exposure in the container logs with the public URL and the local proxy:
Available on the internet:

https://librefolio.yourtailnet.ts.net/
|-- proxy http://127.0.0.1:6040

Press Ctrl+C to exit.
  • Note: At this point, the service is online, but you must wait a few minutes for the MagicDNS record propagation to complete globally.

Disable Key Expiry for the Container

To prevent the sidecar container from expiring and disconnecting from your Tailnet after the default period (180 days):

  1. Go to the Machines page of the Tailscale Admin Console.
  2. Find the container node (e.g., librefolio or tailscale-librefolio) in the list.
  3. Click the three dots (...) icon on the right of the device row.
  4. Select the Disable Key Expiry option.
๐ŸŸข Advantages (Pros) ๐Ÿ”ด Disadvantages (Cons)
  • Ability to create infinite independent public Funnels on a single physical machine.
  • Separate and dedicated URLs for each home service.
  • Local network packets travel securely and directly between the container and the target service.
  • Requires terminal use and manual configuration of Docker Compose files.

๐Ÿ”ฎ MagicDNS and Custom Domains

What is MagicDNS?

MagicDNS automatically assigns a local and public DNS domain name to each of your devices registered in the Tailnet. Instead of having to remember IP addresses like 100.110.222.112, you can type http://your-server in the browser. Public domains assigned by MagicDNS end with the suffix *.ts.net (for example, https://librefolio.your-tailnet.ts.net).

How to Use a Custom Domain with Tailscale

If you own your own personal domain (e.g., mydomain.com) and want to use it to reach your private Tailscale nodes instead of using the standard *.ts.net URL, you can proceed with two main techniques:

This is the simplest solution to access your devices privately using your domain.

  1. Log into your domain registrar's console (e.g., Cloudflare, GoDaddy, Namecheap).
  2. Create a type A (or AAAA for IPv6) DNS record for the chosen subdomain (e.g., librefolio.mydomain.com).
  3. Point the record directly to the private Tailscale IP of your server (e.g., 100.77.72.90).
  4. How it works: Since IP addresses in the 100.64.0.0/10 network are not publicly routable globally, the domain will resolve and work only when you are connected to your Tailscale VPN, ensuring that no external user can access or scan the service. For details, see the Official documentation on DNS settings.

Method 2: Split DNS (With Internal DNS Server)

If you want to dynamically manage internal records and not publish them on the internet:

  1. Configure a private DNS server in your LAN (such as Pi-hole, AdGuard Home, or CoreDNS).
  2. Add local records of your domain pointing them to your Tailscale IPs.
  3. In the Tailscale admin console, go to DNS -> Nameservers -> Add Nameserver and add the Tailscale IP of your private DNS as a global nameserver or restricted to your domain. For details, see the Official documentation on Split DNS.

Caution on Public Funnel Exposure

Since Tailscale public Funnels are exposed on the internet only via the secure *.ts.net domain (thanks to SSL certificates signed by Tailscale), direct CNAME mapping from your custom domain to a Funnel address will cause SSL/TLS security errors in browsers, unless a separate reverse proxy (such as Caddy or Nginx) is used to manage your zone's certificates. The public address of your instance will be librefolio.your-tailnet.ts.net, where the initial part librefolio is automatically defined by the value assigned to the TS_HOSTNAME variable.