# tailscale/golink: Private Go Links for a Tailnet

> golink gives a Tailscale organization a private shortlink service for internal sites and shared resources. It combines a Go server, SQLite storage, tsnet connectivity, and a bundled web interface, making it a poor fit for public or non-Tailscale use.

**tailscale/golink** — A private shortlink service for tailnets

- Repository: https://github.com/tailscale/golink
- Stars: 1,963 · Forks: 149
- Language: Go
- License: BSD-3-Clause
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/tailscale-golink

## Why tailscale/golink Fits an Internal Tailnet

golink addresses a specific problem: naming internal destinations with short URLs that people can remember and share without reconstructing longer links every time. It is intended for members of a Tailscale tailnet and the teams or companies operating that network. The README describes Tailscale's own use of golink across the company, including its role in onboarding, where employees encountered many go links after joining the tailnet.

The important boundary is private access. golink exposes its listening service through Tailscale rather than requiring published internet ports. With MagicDNS enabled, a user can request `http://go/` from a connected device without installing a browser extension or adding a local hosts entry. Devices still need Tailscale installed and connected to the relevant tailnet. This makes golink suited to internal documentation systems, dashboards, administrative tools, and other destinations that should not become public shortlinks.

It is not presented as a general public URL-shortening platform. The README explicitly points readers who want a SaaS go-link service without Tailscale toward `golinks.io` or `trot.to`. That distinction matters because access control, identity, DNS, and deployment depend on the surrounding Tailscale environment rather than on a conventional public web login described by this repository.

Organizations that do not already manage tailnets should account for that dependency before selecting it. The convenience of `go` as a hostname comes from MagicDNS and authenticated Tailscale connectivity, not from a standalone public naming system. A team wanting ordinary links for customers or the general public would be choosing the wrong deployment model.

## golink's Server, Storage, and Tailnet Data Flow

golink is written in Go and is organized around a command under `cmd/golink`. The README states that the compiled program includes the frontend, so the resulting deployment is a single static binary. The repository also contains Go files for the service and database behavior, `schema.sql` for the SQLite schema, templates under `tmpl/`, static assets under `static/`, and Tailwind configuration for the frontend build. Based on this layout, application records and the HTTP interface are part of one deployable Go project rather than separate application services.

Link definitions are stored in SQLite. Production operation requires preserving the SQLite database because it contains the links that users create and resolve. Development mode differs: `-dev-listen :8080` uses a temporary database and does not attempt to join a tailnet. This permits inspection of the local web interface without first configuring `TS_AUTHKEY`.

For normal operation, golink uses Tailscale connectivity through `tsnet`, as reflected by the project topics, the `tailscale.com` dependency, and the README's statement that it registers as a tailnet node. Tailscale state is stored in a `tsnet-golink` directory inside `os.UserConfigDir`. The service can run as a regular node or register as a Tailscale Service by using `--register-as-service=svc:golink` or setting `TS_SERVICE_NAME="svc:golink"`. Service mode is documented as providing a more stable identity, particularly when infrastructure storage may be lost.

The basic resolution path is private and short: a connected user opens `http://go/`, MagicDNS sends the request to the golink node, and golink handles the named link. The README does not document redirect status codes, link collision rules, reserved names, or a complete external API for the web interface. Those details should not be assumed during an adoption review.

## A First Local golink Run on Port 8080

The quickest documented first run uses development mode. From the repository root, start the service with the Go toolchain and `-dev-listen :8080`. The README says that this stores links in a temporary database and avoids joining a tailnet.

```bash
go run ./cmd/golink -dev-listen :8080
```

After the process starts, open the address below in a browser. The developer should see the golink home screen represented by the repository screenshot.

```text
http://localhost:8080/
```

The same development path is available through the published container image:

```bash
docker run -it --rm -p 8080:8080 ghcr.io/tailscale/golink:main -dev-listen :8080
```

This second form also publishes container port 8080 on the host. It is intended for a local development run, not the production arrangement. The production documentation states that no ports need to be exposed because golink listens on port 80 through the Tailscale interface.

This first run will not demonstrate tailnet-only behavior. It also cannot validate identity, ACL enforcement, MagicDNS, Tailscale Service registration, or persistence. Those are production concerns rather than consequences hidden inside `-dev-listen`. The `go.mod` file declares Go 1.26.6, but the README does not give a separate installation procedure for that toolchain.

## Joining golink to a Private Tailnet

A production instance needs an auth key and a SQLite database path. Create the key through the Tailscale administrative settings linked by the README. The documentation recommends adding a tag, such as `tag:golink`, to make ACL control easier and prevent the node from expiring. It also recommends not marking the key as ephemeral, since that can remove the node after it goes offline.

Set the key as `TS_AUTHKEY` when starting the binary and select a writable database file:

```bash
TS_AUTHKEY="tskey-auth-<key>" go run ./cmd/golink -sqlitedb golink.db
```

The first run stores Tailscale data under `tsnet-golink` in the user configuration directory. The README says that the auth key only needs to be supplied on first run as long as this directory remains on persistent storage. Retaining only `golink.db` preserves the links, but losing the tsnet state directory can force the deployment to authenticate again.

For a Tailscale Service identity, the documented startup command is:

```bash
TS_AUTHKEY="tskey-auth-<key>" go run ./cmd/golink -sqlitedb golink.db --register-as-service=svc:golink
```

The equivalent environment-variable form is:

```bash
TS_SERVICE_NAME="svc:golink" TS_AUTHKEY="tskey-auth-<key>" go run ./cmd/golink -sqlitedb golink.db
```

Service mode requires a tagged node, an ACL policy that defines the service and its auto-approvers, and HTTPS on port 443. The README's example grants ownership of `tag:golink` to `autogroup:admin` and lists that tag as an approver for `svc:golink`. It also states that admin capabilities in service mode are resolved through the Tailscale daemon `whois` API and enforced according to ACL policy.

## Persistent Docker Deployment Without Exposed Ports

The production container keeps both important forms of state under `/home/nonroot`: the SQLite database and Tailscale data. A named Docker Compose volume preserves that directory. The documented Compose configuration is:

```yaml
volumes:
  data:

services:
  golink:
    image: ghcr.io/tailscale/golink:main
    container_name: golink
    restart: unless-stopped
    volumes:
      - 'data:/home/nonroot'
```

Initialize the container with an auth key using the following command:

```bash
docker compose run --rm --env 'TS_AUTHKEY=tskey-auth-<key>' golink
```

For a plain Docker deployment, the README gives this command:

```bash
docker run -v /persistent/data:/home/nonroot ghcr.io/tailscale/golink:main
```

The persistent directory must be writable by uid 65532 and gid 65532 because the image runs as `nonroot`. The documented ownership command targets `/persistent/data` directly:

```bash
sudo chown 65532 /persistent/data
```

Running the container as root is listed as an alternative, but that changes the deployment's privilege model and should be an explicit decision. Failure to provide writable persistent storage is also the explanation given by the README for the Docker error `unable to open database file: out of memory (14)`, despite the message mentioning memory. No host port mapping is required for the production arrangement because access is handled through Tailscale.

A Compose deployment should retain the named volume. Removing or pruning the named volume can discard the SQLite links and tsnet state. The Dockerfile uses `restart: unless-stopped` only in the Compose example, so restart behavior should not be assumed for every deployment method.

## Backups, Offline Resolution, and Operational Limits

golink's state model creates a clear backup boundary. Tailscale's production account, described in the README, stores a weekly snapshot of go links in an internal Git monorepo. A wrapper script around `golink --resolve-from-backup` lets people with a local copy of that repository resolve links offline. The `--resolve-from-backup` mechanism is mentioned, but the README does not provide the complete command syntax or repository format.

A deployment should account for both state types separately. SQLite contains the managed links, while the `tsnet-golink` directory contains Tailscale identity and connectivity data. A database backup can support restoration or offline resolution, but it does not eliminate the need for persistent tsnet storage on the live instance. The README's Docker warning about SQLite reinforces this operational boundary.

The project is deliberately tied to Tailscale. It is the wrong tool when recipients do not run Tailscale, when access must be available from the public internet, or when MagicDNS cannot be enabled. Its service mode also adds an HTTPS-on-443 requirement and ACL setup rather than removing that dependency. Development mode bypasses tailnet joining, but its temporary database is unsuitable for state that must survive restarts.

The repository has no GitHub releases, so the README's `:main` image does not point to a documented fixed release tag. That image reference can move as the branch changes. The repository's last push was on 2026-10-09. That date gives operators a concrete point for checking recent source changes, but the README does not define a version compatibility or upgrade policy for the container image.

## Build Toolchain and BSD-3-Clause Upgrade Cost

The module declares Go 1.26.6, Tailwind CSS dependencies, and `modernc.org/sqlite`. The Dockerfile downloads dependencies and builds `./cmd/golink` with `CGO_ENABLED=0`, producing a static binary for the distroless `nonroot` image. This reduces the runtime artifact to the binary plus persistent state, although it does not remove deployment or upgrade work.

After dependency changes alter `go.mod` and `go.sum`, the README asks maintainers to refresh `flake.nix`. The documented command is:

```bash
./update-flake.sh
```

That requirement indicates a reproducible-build maintenance step beyond a routine Go dependency update. Operators who only consume the container image do not necessarily need to run the script, but anyone maintaining a fork or source-based build should account for it.

golink is licensed under BSD-3-Clause. That identifier permits reuse and redistribution under the license's conditions, but the README does not reproduce the license text. Anyone distributing a modified build should review the `LICENSE` file and preserve the applicable notices. This is an operational licensing check, not legal advice.

Upgrades also need to cover persisted SQLite data and Tailscale identity state. The README does not document a database migration or rollback procedure, and the repository has no GitHub releases. A cautious source deployment should therefore inspect schema changes and keep the link database backed up before replacing the binary or moving to a newer `ghcr.io/tailscale/golink:main` image. Anyone pinning the image for production must choose a stable reference based on the repository's actual tags because the documented `main` reference does not promise immutability.

## Conclusion

Organizations already using Tailscale should consider golink when internal URLs need stable, memorable names and users are expected to run Tailscale. It should not be selected for a public SaaS, users outside the tailnet, or deployments that cannot preserve SQLite and tsnet state. Before adoption, verify that `TS_AUTHKEY` handling, ACL policy, MagicDNS, writable persistent storage, backup coverage, and image update policy match the organization's operating requirements.

## FAQ

### How do I run tailscale/golink locally for development?

Run `go run ./cmd/golink -dev-listen :8080`, then open `http://localhost:8080/`. This mode uses a temporary database and does not join a tailnet.

### What does tailscale/golink need in a production deployment?

It needs a Tailscale auth key, a persistent SQLite database, and persistent tsnet data under `os.UserConfigDir`. With Tailscale and MagicDNS, users can access it as `http://go/` from connected devices.

### Can tailscale/golink be used outside Tailscale?

Development mode can run on localhost without joining a tailnet, but it stores links temporarily. A persistent production service is designed for access through a tailnet, and users need Tailscale installed and connected.

## Sources

- [Issues](https://github.com/tailscale/golink/issues)
- [License: BSD-3-Clause](https://github.com/tailscale/golink/blob/main/LICENSE)
- [README](https://github.com/tailscale/golink/blob/main/README.md)
- [tailscale/golink on GitHub](https://github.com/tailscale/golink)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tailscale-golink
