matcha

package module
v0.12.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 20, 2026 License: MIT Imports: 20 Imported by: 0

README

Matcha

Deploy Docker apps with automatic SSL and zero-downtime updates. Powered by kamal-proxy.

How it works

Internet → matcha-proxy (ports 80/443, TLS) → app containers (internal ports)
                 ↓
         routes by hostname
         ↓              ↓              ↓
    fusionaly:8080  plausible:8000  gitea:3000

One shared kamal-proxy routes traffic by hostname. Each app runs as a separate Docker container on matcha-network. TLS is automatic via Let's Encrypt.

On deploy, kamal-proxy health-checks the new container before switching traffic. If unhealthy, traffic stays on the old container. Zero downtime, even with a single replica.

Two ways to use Matcha

As a CLI — deploy any Docker image
# Install matcha on your server
curl -fsSL https://raw.githubusercontent.com/karloscodes/matcha/main/install.sh | sh

# Set up shared infrastructure
matcha setup

# Add and deploy apps
matcha add plausible --image plausible/analytics:latest --domain analytics.example.com --port 8000 --env SECRET_KEY=abc123
matcha deploy plausible

matcha add gitea --image gitea:latest --domain git.example.com --port 3000 --volume /data
matcha deploy gitea

# Day-to-day
matcha list                    # show all apps
matcha update plausible        # pull latest, zero-downtime redeploy
matcha status plausible        # container details
matcha logs plausible          # stream logs
matcha exec plausible sh       # shell into container
matcha remove plausible        # stop and unregister
As a library — build a self-deploying binary

Import Matcha into your Go project to get a single binary that installs, updates, and manages itself:

package main

import (
    "fmt"
    "os"

    "github.com/karloscodes/matcha"
)

var version = "dev"

func main() {
    m := matcha.New(matcha.Config{
        Name:     "myapp",
        AppImage: "ghcr.io/user/myapp:latest",

        // Optional
        AppPort:        8080,
        HealthPath:     "/up",
        Volumes:        []string{"/app/storage"},
        CronUpdates:    true,
        ManagerRepo:    "user/myapp",
        ManagerVersion: version,
    })

    if len(os.Args) < 2 {
        fmt.Println("Usage: myapp <install|update|status|exec>")
        os.Exit(1)
    }

    var err error
    switch os.Args[1] {
    case "install":
        err = m.Install()
    case "update":
        err = m.Update()
    case "status":
        err = m.Status()
    case "exec":
        err = m.Exec(os.Args[2:]...)
    }

    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}
myapp install    # Docker + proxy + app, all in one command
myapp update     # pull latest, zero-downtime redeploy

Both ways use the same shared proxy and config. An app installed via myapp install shows up in matcha list.

Updates

Set CronUpdates: true (Go library) or use a cron job to run matcha update <name> nightly. Here's what happens:

  1. docker pull checks the remote image digest — if the :latest tag hasn't changed, no download happens
  2. If there's a new image, the new container starts and kamal-proxy health-checks it
  3. Once healthy, traffic switches to the new container. The old one is removed
  4. If Backups: true, a SQLite backup is created before each deploy (keeps last 3)

Updates are cheap when nothing changed — just a digest check.

Self-update

Matcha binaries can update themselves from GitHub releases.

As a CLI

matcha update <name> checks for a newer matcha release at karloscodes/matcha before updating the app. If a new version exists, the binary at /usr/local/bin/matcha is replaced and the process re-execs to continue with the new code.

As a library

When you set ManagerRepo and ManagerVersion, Update() checks your repo's GitHub releases for a newer binary before updating the Docker image.

myapp update
  → check github.com/user/myapp/releases for newer version
  → download myapp-linux-amd64, verify checksum
  → replace /usr/local/bin/myapp
  → re-exec: new binary continues the update
  → pull Docker image, zero-downtime redeploy

To enable this:

  1. Set ManagerRepo and ManagerVersion in your config:
m := matcha.New(matcha.Config{
    Name:           "myapp",
    AppImage:       "ghcr.io/user/myapp:latest",
    ManagerRepo:    "user/myapp",
    ManagerVersion: version, // set via ldflags at build time
})
  1. Build with version injected:
go build -ldflags "-X main.version=1.2.3" -o myapp ./cmd/myapp/
  1. Publish releases with GoReleaser (or equivalent) that produce:
    • myapp-linux-amd64, myapp-linux-arm64 (raw binaries, no archives)
    • checksums.txt (SHA256)

See .goreleaser.yml in this repo for an example config.

CLI commands

Command Description
matcha setup Install Docker, create network, start shared proxy
matcha add <name> --image --domain [--port] [--volume] [--health-path] [--env KEY=VAL] Register a new app
matcha deploy <name> Pull image and deploy
matcha update <name> Pull latest image, zero-downtime redeploy
matcha list Show all apps
matcha status <name> Container details
matcha logs <name> Stream app logs
matcha exec <name> <cmd> Run command in container
matcha remove <name> Stop and unregister
matcha migrate <name> Migrate from old per-app layout

Config (library)

Field Default Description
Name required App name (container name, env prefix)
AppImage required Docker image to deploy
AppPort 8080 Port your app listens on
HealthPath /up Health check endpoint (must return 200)
Volumes [] Container paths to mount (e.g., /app/storage)
CronUpdates false Daily 3 AM auto-update cron job
Backups false SQLite backup before each deploy (keeps last 3)
ProxyImage basecamp/kamal-proxy:latest kamal-proxy image
ManagerRepo "" GitHub repo for self-updating the binary
ManagerVersion "" Current version (set via ldflags)

On-disk layout

/etc/matcha/config.yml          # all app config + env vars (single file)

/var/matcha/
├── fusionaly/
│   └── storage/                # volume data → /app/storage
├── plausible/
│   └── data/                   # volume data → /data
└── proxy/                      # kamal-proxy TLS certs and state

Volumes are auto-resolved from container paths: /app/storage becomes /var/matcha/{name}/storage:/app/storage.

Environment variables

Matcha auto-generates these for each container:

Variable Example
{NAME}_DOMAIN MYAPP_DOMAIN=app.example.com
{NAME}_APP_PORT MYAPP_APP_PORT=8080
{NAME}_ENV MYAPP_ENV=production

A PRIVATE_KEY is generated on first install. Additional env vars can be set via --env flag or directly in /etc/matcha/config.yml.

Migration from old layouts

If upgrading from the old per-app layout (/etc/matcha/apps/{name}/ or /opt/{name}/):

# Auto-migrates on first update
myapp update

# Or explicitly
matcha migrate myapp

Config and env vars are moved to /etc/matcha/config.yml, data to /var/matcha/{name}/.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConfigPath added in v0.12.0

func ConfigPath() string

ConfigPath returns the path to the matcha config file.

func DataDir added in v0.12.0

func DataDir(name string) string

DataDir returns the data directory for an app.

func GeneratePrivateKey added in v0.12.0

func GeneratePrivateKey() (string, error)

GeneratePrivateKey creates a secure random key.

func ListApps added in v0.12.0

func ListApps() (map[string]AppConfig, error)

ListApps returns all apps from the YAML config, sorted by name.

func ListAppsFrom added in v0.12.0

func ListAppsFrom(path string) (map[string]AppConfig, error)

ListAppsFrom returns all apps from a specific config path.

func ListAppsSorted added in v0.12.0

func ListAppsSorted(apps map[string]AppConfig) []string

ListAppsSorted returns app names sorted alphabetically.

func RemoveApp added in v0.12.0

func RemoveApp(name string) error

RemoveApp removes an app from the YAML config.

func RemoveAppFrom added in v0.12.0

func RemoveAppFrom(path, name string) error

RemoveAppFrom removes an app from a specific config file.

func ResolveVolumes added in v0.12.0

func ResolveVolumes(name string, volumes []string) []string

ResolveVolumes converts container paths to docker -v args using default data dir. "/app/storage" → "/var/matcha/{name}/storage:/app/storage" "/data" → "/var/matcha/{name}/data:/data" "/var/lib/plausible" → "/var/matcha/{name}/plausible:/var/lib/plausible"

func SaveApp added in v0.12.0

func SaveApp(name string, app AppConfig) error

SaveApp updates a single app in the YAML config (read-modify-write).

func SaveAppTo added in v0.12.0

func SaveAppTo(path, name string, app AppConfig) error

SaveAppTo updates a single app in a specific config file.

func SaveMatchaConfig added in v0.12.0

func SaveMatchaConfig(cfg *MatchaConfig) error

SaveMatchaConfig writes the YAML config file.

func SaveMatchaConfigTo added in v0.12.0

func SaveMatchaConfigTo(cfg *MatchaConfig, path string) error

SaveMatchaConfigTo writes the YAML config to a specific path.

func Setup added in v0.12.0

func Setup() error

Setup installs shared infrastructure: Docker, network, and proxy.

Types

type AppConfig added in v0.12.0

type AppConfig struct {
	Image      string            `yaml:"image"`
	Domain     string            `yaml:"domain"`
	Port       int               `yaml:"port,omitempty"`
	HealthPath string            `yaml:"health_path,omitempty"`
	Volumes    []string          `yaml:"volumes,omitempty"`
	Env        map[string]string `yaml:"env,omitempty"`
}

AppConfig holds the configuration for a single deployed application.

func LoadApp added in v0.12.0

func LoadApp(name string) (AppConfig, error)

LoadApp loads a single app from the YAML config.

func LoadAppFrom added in v0.12.0

func LoadAppFrom(path, name string) (AppConfig, error)

LoadAppFrom loads a single app from a specific config path.

type BackupFile

type BackupFile struct {
	Name      string
	Path      string
	CreatedAt time.Time
	Size      int64
}

BackupFile represents a database backup file.

type Config

type Config struct {
	// Required
	Name     string // "fusionaly" → env prefix FUSIONALY_, container names, etc.
	AppImage string // "karloscodes/fusionaly:latest"

	// Optional with defaults
	BinaryPath string // default: /usr/local/bin/{Name}
	ProxyImage string // default: basecamp/kamal-proxy:latest
	HealthPath string // default: /up
	AppPort    int    // default: 8080

	// Feature flags
	CronUpdates bool // daily 3 AM auto-update cron job
	Backups     bool // SQLite backup with retention policy

	// Custom configuration
	Volumes []string // Container paths to mount (e.g., /app/storage)

	// Self-update configuration (see selfupdate.go for conventions)
	ManagerRepo    string // GitHub repo for releases, e.g., "karloscodes/fusionaly"
	ManagerVersion string // current version, e.g., "v1.4.37" (set via ldflags at build time)

	// Internal: override paths (for testing)
	ConfigPath  string
	DataDirBase string
}

Config defines how Matcha deploys your application.

type Matcha

type Matcha struct {
	// contains filtered or unexported fields
}

Matcha is the main orchestrator for deployments.

func New

func New(cfg Config) *Matcha

New creates a new Matcha instance with the given configuration.

func (*Matcha) AppContainerName

func (m *Matcha) AppContainerName() string

AppContainerName returns the app container name.

func (*Matcha) BackupDB

func (m *Matcha) BackupDB() (string, error)

BackupDB creates a backup of the database and returns the backup path.

func (*Matcha) DataDir added in v0.12.0

func (m *Matcha) DataDir() string

DataDir returns the data directory for this app.

func (*Matcha) Deploy

func (m *Matcha) Deploy() error

Deploy triggers a deployment with current configuration.

func (*Matcha) EnvPrefix

func (m *Matcha) EnvPrefix() string

EnvPrefix returns the uppercase name used for environment variables.

func (*Matcha) Exec

func (m *Matcha) Exec(args ...string) error

Exec runs a command inside the app container.

func (*Matcha) GetConfig

func (m *Matcha) GetConfig() Config

GetConfig returns the current configuration.

func (*Matcha) GetDomain

func (m *Matcha) GetDomain() (string, error)

GetDomain reads the domain from config or YAML.

func (*Matcha) Install

func (m *Matcha) Install() error

Install runs the full installation process.

func (*Matcha) Logs added in v0.12.0

func (m *Matcha) Logs() error

Logs streams logs from the app container.

func (*Matcha) Migrate added in v0.12.0

func (m *Matcha) Migrate() error

Migrate moves from old layouts to new YAML config + /var/matcha/{name}/ data dir. Supports migration from:

  • /opt/{name}/.env (legacy layout)
  • /etc/matcha/apps/{name}/app.json + .env (previous multi-app layout)

func (*Matcha) NetworkName

func (m *Matcha) NetworkName() string

NetworkName returns the Docker network name.

func (*Matcha) ProxyContainerName added in v0.11.0

func (m *Matcha) ProxyContainerName() string

ProxyContainerName returns the proxy container name.

func (*Matcha) Reload

func (m *Matcha) Reload() error

Reload restarts containers with current config (no image pull).

func (*Matcha) RemoveFromProxy added in v0.12.0

func (m *Matcha) RemoveFromProxy() error

RemoveFromProxy removes the service from kamal-proxy.

func (*Matcha) RestoreDB

func (m *Matcha) RestoreDB() error

RestoreDB lists backups and restores the selected one.

func (*Matcha) SaveImage

func (m *Matcha) SaveImage() error

SaveImage persists the current app image to the YAML config.

func (*Matcha) SelfUpdate

func (m *Matcha) SelfUpdate() (bool, error)

SelfUpdate checks for a newer manager version and updates if available. Returns true if an update was performed.

func (*Matcha) SetImage

func (m *Matcha) SetImage(image string)

SetImage changes the app image for subsequent deployments.

func (*Matcha) StartSpinner

func (m *Matcha) StartSpinner(name string) *Spinner

StartSpinner creates and starts an animated spinner for a step.

func (*Matcha) Status

func (m *Matcha) Status() error

Status shows the current state of the deployment.

func (*Matcha) StopApp added in v0.12.0

func (m *Matcha) StopApp() error

StopApp stops and removes the app container.

func (*Matcha) Update

func (m *Matcha) Update() error

Update pulls the latest image and performs a deployment.

type MatchaConfig added in v0.12.0

type MatchaConfig struct {
	Apps map[string]AppConfig `yaml:"apps"`
}

MatchaConfig is the top-level structure of /etc/matcha/config.yml.

func LoadMatchaConfig added in v0.12.0

func LoadMatchaConfig() (*MatchaConfig, error)

LoadMatchaConfig reads the YAML config file.

func LoadMatchaConfigFrom added in v0.12.0

func LoadMatchaConfigFrom(path string) (*MatchaConfig, error)

LoadMatchaConfigFrom reads a YAML config from a specific path.

type Spinner

type Spinner struct {
	// contains filtered or unexported fields
}

Spinner handles animated progress display.

func (*Spinner) Stop

func (s *Spinner) Stop(success bool)

Stop stops the spinner and shows success or failure.

Directories

Path Synopsis
cmd
matcha command
Package testrunner provides utilities for running integration tests in isolated environments using OrbStack VMs.
Package testrunner provides utilities for running integration tests in isolated environments using OrbStack VMs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL