tailscale/golink: Private Go Links for a Tailnet
A private shortlink service for tailnets
At a glance
- What is it?
- 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.
- Who is it for?
- 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.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
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.
go run ./cmd/golink -dev-listen :8080After the process starts, open the address below in a browser. The developer should see the golink home screen represented by the repository screenshot.
http://localhost:8080/The same development path is available through the published container image:
docker run -it --rm -p 8080:8080 ghcr.io/tailscale/golink:main -dev-listen :8080This 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:
TS_AUTHKEY="tskey-auth-<key>" go run ./cmd/golink -sqlitedb golink.dbThe 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:
TS_AUTHKEY="tskey-auth-<key>" go run ./cmd/golink -sqlitedb golink.db --register-as-service=svc:golinkThe equivalent environment-variable form is:
TS_SERVICE_NAME="svc:golink" TS_AUTHKEY="tskey-auth-<key>" go run ./cmd/golink -sqlitedb golink.dbService 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:
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:
docker compose run --rm --env 'TS_AUTHKEY=tskey-auth-<key>' golinkFor a plain Docker deployment, the README gives this command:
docker run -v /persistent/data:/home/nonroot ghcr.io/tailscale/golink:mainThe 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:
sudo chown 65532 /persistent/dataRunning 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:
./update-flake.shThat 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.
Editorial 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.
Frequently asked questions
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.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tailscale-golink)Was this useful?