HomeProjectsBlogContact
~/Blog

Want to chat? root.vewake@gmail.com

© 2026 Vivek Patil

~/Blog
Aug 2026
7 min read

Building and Hosting Apps over SSH


How to Build and Host an Interactive Terminal App over SSH

Terminal user interfaces (TUIs) have experienced a massive renaissance. Beyond standard CLI tools, you can build full interactive experiences—dashboards, games, portfolios, and blogs—and serve them to anyone in the world directly over SSH without requiring them to install any dependencies locally.

You can try a live example in your terminal right now:

bash
ssh ssh.vewake.me

(Web version: vewake.me)

Here is a practical guide on how to build and host your own SSH-accessible terminal application.


The Architecture & Toolkit#

Building an SSH application requires two parts: an SSH Server Daemon to handle network sessions and an Interactive TUI Runtime to render the interface.

In the Go ecosystem, the Charm stack provides everything you need:

  1. Wish: A lightweight SSH server engine in Go. It manages SSH handshakes, PTY allocations, window resizing (SIGWINCH), and user session isolation.
  2. Bubble Tea: A TUI framework based on The Elm Architecture (Model-Update-View). It turns keystrokes and window events into state changes and clean UI re-renders.
  3. Lip Gloss: A CSS-like styling library for terminals (colors, margins, borders, alignments).
  4. Glamour: In-terminal Markdown renderer for rich text content and code highlighting.

1. Building the Bubble Tea Application#

The core of any Bubble Tea application revolves around three functions:

go
type model struct {
activeTab int
cursor int
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "j", "down":
m.cursor++
case "k", "up":
if m.cursor > 0 { m.cursor-- }
case "tab":
m.activeTab = (m.activeTab + 1) % 4
}
}
return m, nil
}
func (m model) View() string {
return lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Render("Interactive SSH Application Content")
}

2. Wrapping it with the Wish SSH Daemon#

Wish connects an incoming SSH session directly into your Bubble Tea model:

go
package main
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
wishbubbletea "github.com/charmbracelet/wish/bubbletea"
"github.com/muesli/termenv"
)
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
pty, _, active := s.Pty()
if !active {
wish.Fatalln(s, "no active terminal allocated")
return nil, nil
}
// Ensure full 24-bit TrueColor profile is sent to the client
lipgloss.SetColorProfile(termenv.TrueColor)
m := newAppModel()
return m, []tea.ProgramOption{
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
}
}
func main() {
s, _ := wish.NewServer(
wish.WithAddress("0.0.0.0:22"),
wish.WithHostKeyPath(".ssh/id_ed25519"),
wish.WithMiddleware(wishbubbletea.Middleware(teaHandler)),
)
s.ListenAndServe()
}

3. Key Tip: Enforcing TrueColor over SSH#

When applications run as background services (e.g. systemd or Docker), they often lack an attached TTY on the host process. This can cause terminal style detection to fall back to a 1-bit monochrome (black & white) mode.

To ensure your users always get rich 24-bit RGB colors, explicitly force the TrueColor profile on every session:

go
lipgloss.SetColorProfile(termenv.TrueColor)

4. Hosting on Port 22 (The Dual-Port Setup)#

To allow visitors to connect with standard ssh yourdomain.com without needing a -p port flag, your app must bind to Port 22.

On Linux servers (AWS, DigitalOcean, Hetzner, etc.), standard OpenSSH (sshd) already listens on port 22. To avoid locking yourself out:

  1. Move OpenSSH to Port 2222: Update /etc/ssh/sshd_config to Port 2222 so you can always log into the server for administration (ssh -p 2222 user@host).
  2. Run your TUI on Port 22: Run your compiled binary as a systemd service or Docker container binding to 0.0.0.0:22.
  3. DNS: Add an A record in your DNS provider pointing yourdomain.com directly to your server's public IP (ensure HTTP proxying is disabled since SSH is raw TCP traffic).

Live Demo & Conclusion#

Building apps and portfolios over SSH provides an ultra-fast, nostalgic, and dependency-free experience for users. With Go, Wish, and Bubble Tea, you can turn any CLI idea into a globally accessible terminal experience in an afternoon.

  • SSH Version: ssh ssh.vewake.me
  • Web Version: vewake.me
Vivek Patil

Vivek Patil

Crafting software, ricing Linux & building local-first tools.

All posts
·