Config lives at ~/.config/seikan/config.json (override with SEIKAN_CONFIG), storing the server, the client key, the declared tunnels, and a per-tunnel connect-token cache. The one exception is up, the container entrypoint, which takes everything from the environment and reads and writes nothing.
Getting a client key onto a machine has two shapes. seikan init --server <host> asks the server to let this machine in, prints a confirmation code, and waits for an administrator to approve it — nothing secret travels between machines, and Ctrl-C is safe: re-running resumes the same request. seikan init --server <host> --api-key sk_… uses a key an administrator issued and gave you directly.
There are two ways to run a tunnel. add + start is the persistent one: tunnels are declared in the config and served by a background OS service that survives logout and restarts on failure. serve is the ad-hoc one: a single tunnel in the foreground, gone when you Ctrl-C.
In a container it is neither: up serves one tunnel described entirely by environment variables, with no config file to write and no setup step to run first. See below.
Managing tunnels
| Command | Description |
|---|---|
add <domain[,…]> <port> |
Add an HTTP tunnel. Creates the frontend on the server immediately — so a rejected domain or an exhausted quota surfaces now, not at first start — declares it in the config, and restarts the background service if one is running, so the tunnel comes up with no further step. |
add --tcp <port> [--name X] |
Add a TCP tunnel (the server auto-assigns a public port). |
add --peer --name X <port> |
Add a peer tunnel (provider side). Prints the one-time consumer token and a ready-to-run forward command right here, which is the only time it is ever shown. |
remove <domain|name> |
Stop serving a tunnel and delete its frontend, clearing the cached token. --keep-remote leaves the frontend on the server. Aliases: rm, delete. |
list |
List declared tunnels with their live status (key, type, target, public endpoint, online, cert). Frontends that exist on the server but aren't declared here are listed separately. Works without a reachable server — the live columns are simply blank. |
Managing the background service
Linux uses a per-user systemctl --user unit, macOS a launchd LaunchAgent. Both run as your own user — no root.
| Command | Description |
|---|---|
start |
Install the service if it isn't there (or its definition is out of date), then start and enable it. On Linux, sudo loginctl enable-linger $USER makes it survive logout. |
stop |
Stop and disable the service. |
restart |
Restart it. Also repairs an out-of-date service definition. |
status |
Whether the service is running and enabled. |
logs [-f] [-n 100] |
Stream its log: journalctl --user -u seikan on Linux, the LaunchAgent log file on macOS. |
run [keys…] |
Run all (or named) declared tunnels in the foreground, each with its own auto-reconnect. This is what the service invokes; use it directly for non-systemd setups (e.g. containers). |
Everything else
| Command | Description |
|---|---|
init |
Enrol this machine with a server. Without --api-key it requests approval: it prints a confirmation code and waits while an administrator approves it in the admin UI (Requests), then saves the client key that approval earned — nothing secret has to travel between machines. With --api-key <sk_…> it uses a key an administrator handed you, verifying it against the server before saving. Flags: --server <host|url> (prompted if omitted), --api-key, --insecure. |
serve <domain[,…]> <port> |
Create/resolve an HTTP frontend, wait for its cert, and tunnel <domain> → local <port> (or host:port). The domain argument may be a comma-separated list, and entries may be wildcards (*.example.com). |
serve --tcp <port> |
TCP frontend (server auto-assigns a public port, printed on connect). --name <key> caches it (default tcp:<target>). |
serve --peer --name <key> <port> |
Private peer frontend — no domain, no public port. Registers local <port> as the backend another client can reach through the server. On first creation, prints a one-time consumer token and a ready-to-run forward command; that's the only time the consumer token is shown, so capture it then. See "Private peer tunnels" below. |
exec-install [--bin D] |
Copy this binary onto your PATH (--bin <dir>, else $SEIKAN_BIN_DIR, /usr/local/bin, ~/.local/bin). The self-install convention the install.sh bootstrap invokes; the binary installs itself under its own filename (install is an accepted alias). |
exec-uninstall [--yes] |
Remove this binary from PATH (the matching convention; --yes skips the prompt). |
uninstall [--purge] |
Remove the per-user background service. --purge also removes the saved config (~/.config/seikan) and this binary. |
configure |
Open the config (~/.config/seikan/config.json) in $EDITOR; on save, restart the background service if one is installed. |
update [--check] [--no-restart] |
Check the site saved at install time (or SEIKAN_SITE) for a newer release; if one exists, download + SHA-256-verify it, replace this binary in place, and restart the background service if one is installed. --check reports the available version without installing it. |
up |
Serve one tunnel described entirely by SEIKAN_* env vars — the client image's entrypoint. Creates its frontend on first start and reuses it afterwards, with no config file and nothing written to disk. See "Running in a container" below. |
connect |
Low-level raw tunnel, no config/admin API: --server <host:port> --token <sk_t_…> --target <host:port>. |
forward |
Low-level peer tunnel consumer, no config/admin API: --server <host:port> --token <sk_t_…> --listen <port>. Listens locally and relays each connection through the server to whatever peer frontend the consumer token belongs to. |
version / help |
Print version / show help. |
Common flags
| Flag | Env | Meaning |
|---|---|---|
--insecure |
SEIKAN_INSECURE |
Skip server TLS verification (dev / staging certs). |
--verbose, -v |
SEIKAN_VERBOSE |
Log each request (method, path, status, timing) — one line per request, even when keep-alive packs many requests onto a single stream. The server access log is also authoritative per request. |
--recreate |
— | serve only: delete + recreate the frontend if it already exists. |
Note: Reconnect —
serve/connectreconnect automatically with backoff if the tunnel drops (e.g. after a server update) and recover within ~1s. A rejected token exits immediately. Ctrl-C stops cleanly.
Running in a container
up exists because the rest of the CLI assumes a machine someone logs into: init writes a config file and everything else reads it back. A container has no such file — and the published image is distroless with no writable home — so up takes the whole tunnel from the environment and does the admin-API work itself on start.
| Variable | Meaning |
|---|---|
SEIKAN_SERVER |
Admin host or URL. Required. |
SEIKAN_API_KEY |
A client key (sk_…). Lets the container create and keep its own frontend. |
SEIKAN_DOMAIN |
Domain(s) to serve, comma-separated; wildcards allowed. |
SEIKAN_TCP_PORT |
Public TCP port, instead of SEIKAN_DOMAIN. The same port is requested on every start. |
SEIKAN_TARGET |
The service to forward to, host:port. Required — a bare port is refused, because in a container localhost is the container. |
SEIKAN_TOKEN |
A connect token you minted yourself, instead of SEIKAN_API_KEY; then no domain or port is needed, and no admin API call is made. If both are set, the key wins. |
SEIKAN_INSECURE · SEIKAN_VERBOSE |
As elsewhere. |
Exactly one of SEIKAN_DOMAIN / SEIKAN_TCP_PORT (with a client key); every value also has a flag, so up is usable outside a container.
On start it finds the frontend its environment describes and rotates its connect token, or creates the frontend if there isn't one. Rotating rather than recreating is what makes a restart cheap: the frontend keeps its ID and its route, so the public side never stops resolving. A token is only ever shown once, at creation, so a container with no state has no other way back in — and the previous token stops working the moment it rotates, which is why exactly one container should run any given frontend.
It exits non-zero, without retrying, on anything retrying cannot fix: a rejected key, a domain or port owned by another client key, a filled quota. An admin API that simply isn't up yet is retried with backoff, since in a compose file the client and server start in the same second.
Private peer tunnels
A peer frontend relays a connection between two clients through the server — nothing is ever exposed publicly, not even briefly. Useful for reaching a database or internal service on one box from another, without opening a public port for it.
It has two independent one-time secrets, minted together and never interchangeable:
- Provider token — registers the backend (the box with the actual service). Works exactly like a TCP frontend's token:
serve --peercaches it locally and reconnects automatically. - Consumer token — relays a connection into the frontend. It is never cached locally; it's printed once, at creation time, for you to hand to whatever needs to reach the tunnel. Holding only the consumer token never lets you register a fake backend, so sharing it is safe even for something like a database credential.
# Box A — has the service (e.g. Postgres on :5432):
seikan serve --peer --name pgdb 5432
Created peer frontend "pgdb". Share this with whatever should reach it (never shown again):
seikan forward --server tunnel.example.com:443 --token sk_t_... --listen 5432
# Box B — wants to reach it:
seikan forward --server tunnel.example.com:443 --token sk_t_... --listen 5432
psql -h localhost -p 5432
Box B needs no client config or client key — just the printed command. A peer frontend never binds a public port and needs no domain or TLS certificate; list/rm on box A manage it like any other frontend.