Open source · MIT · Single Go binary

Publish to your VPS.
One command.

Mason is a small local CLI for publishing to a personal server behind Caddy: deploy static sites and share files. It remembers the host, the paths, the user, and the rsync flags — so you don't. No daemon runs on the server; the remote is just a filesystem contract plus Caddy.

go install github.com/norlinga/mason/cmd/mason@latest

~/sites/blog  —  mason
# deploy the build tree to the configured target mason deploy target blog rsync → mason@share.example.com:/srv/sites/blog ✓ 142 files · 12 changed · 3.2 MB in 1.8s # share a file and get a short link back mason share ./talk.mp3 ✓ uploaded talk.mp3 (4.1 MB) https://share.example.com/s/f76 mason ls inbox/talk.mp3 s/f76 4.1 MB public podcasts/ep-01.mp3 s/a14 8.8 MB public

The premise

Getting bits onto the server is easy. Remembering how is the hard part.

Building a static site is a single command. Then comes the friction: the hostname, the user, the remote path, the SSH key, the exact rsync flags. Put it in a Makefile and every project re-implements the same script — and they all go stale the moment the server changes.

The idea

A mason assembles blocks into something useful — even beautiful. Mason assembles your files onto your server, turning common publishing tasks into single, memorable commands.

Configuration lives in a .mason file — per-project, or in your home directory — so the context travels with the work. When the server changes, you fix it in one place.

Configure once

The context travels with the work

Named targets hold the host, user, remote root, and base URL. A project .mason picks one; Mason walks up from the cwd to find it, falling back to ~/.mason.

No server daemon

The remote is just files and Caddy

Nothing runs on the VPS but ssh, rsync, and Caddy. Mason talks to a plain filesystem layout and a Mason-owned Caddy snippet — nothing to keep alive, nothing to patch.

Plain & scriptable

Readable by humans and machines

Output is columnar and greppable. Add --json to any command for the machine-readable counterpart, and --dry-run to see the plan before anything moves.

What Mason does

Two jobs, done cleanly.

Deploy a static site, or share a single file. Both land on the same server, behind the same Caddy, addressed by clean URLs.

deploy  static sites

Point Mason at a build directory and a target. mason deploy rsyncs the tree, showing the plan, live progress, and a summary. Use --delete to mirror exactly — removals are confirmed before they happen.

  • mason status shows the deploy plan without transferring a byte.
  • Multiple named targets — deploy the same project to staging or prod.

share  single files

mason share ./talk.mp3 uploads the file and hands back a shareable URL. Send a recording or a PDF without routing it through a chat app — it lives at a clean link on your own domain.

  • Each share gets an object path and a short slug — s/<slug>.
  • Content-addressed blobs: re-sharing identical bytes links, never re-uploads.

Local-first

One binary on your machine. No agent, no control plane — just ssh and rsync under the hood.

Idempotent setup

remote-init prints a script that converges the server to a known state. Re-run it anytime — it's safe.

Self-checking

mason doctor verifies SSH, permissions, the manifest, private-prefix 404s, and Caddy reconciliation.

Quickstart

Set up once, then publish.

Onboard locally, bootstrap the server, verify, and you're deploying sites and sharing files.

01

Local onboarding

mason setup generates an SSH key if needed, writes ~/.mason, and tests the connection. Run it without flags for an interactive prompt.

setup
mason setup \ --host share.example.com \ --user mason \ --root /srv/share \ --base-url https://share.example.com ✓ key ready ~/.ssh/id_mason ✓ wrote ~/.mason ✓ connection ok
02

Bootstrap the server

remote-init prints an idempotent script: it creates the mason user, installs your key, lays out the trees, and adds a Mason-owned Caddy snippet. Review it, then pipe it to an admin login.

remote-init
# review the script, then run it on the server mason remote-init | ssh root@share.example.com sh # …or let mason run it over SSH as an admin mason remote-init --apply --as-admin root ✓ converged user · trees · caddy snippet
03

Deploy a site

Scaffold a project .mason, preview the plan, then deploy the build tree.

deploy
mason init ✓ wrote .mason (target: blog) mason status plan 12 to send · 1 to delete · 129 unchanged mason deploy ✓ deployed 142 files · 3.2 MB · 1.8s
04

Share a file

Drop a file on the server and get a link. Use --as for an exact object path, or let Mason choose a default.

share
mason share ./talk.mp3 --as podcasts/ep-01.mp3 ✓ uploaded podcasts/ep-01.mp3 mason url s/a14 https://share.example.com/s/a14 mason info podcasts/ep-01.mp3 path podcasts/ep-01.mp3 slug s/a14 size 8.8 MB enabled

Core concepts

A small model, deliberately.

Targets say where. Deploys and shares say what. The server contract ties it together.

Concept · 1

Targets

A named destination: host, user, remote root, and base URL. Stored in ~/.mason or a project .mason. One is the default; override per command with -t.

Concept · 2

Shares are links

A share is a link to a content-addressed blob, not a copy. Two endpoints address it independently — an object path and a short slug — and you can move, disable, or remove either.

Concept · 3

The remote contract

The server is a known directory layout plus a Mason-owned Caddy snippet. Private prefixes like /objects/* and /.mason/* return 404 — the blob store stays out of view.

Re-sharing never re-uploads

Blobs are addressed by their content. Share identical bytes to a new path and Mason adds a link only — no second copy on disk, no second upload over the wire.

Reversible by design

disable 404s an endpoint without deleting it; enable brings it back. prune garbage-collects orphaned blobs — dry-run by default, -y to commit.

CLI reference

The whole surface, on one page.

Add --json to any command for machine-readable output, -n/--dry-run to preview, and -t to pick a target.

P Publish

mason init

Scaffold a project .mason.

mason status

Show the deploy plan — no transfer.

mason deploy [target] [--delete]

Rsync the build tree: plan → progress → summary. --delete mirrors exactly (confirmed).

mason share <file> [--as path]

Upload a file; print its shareable URL.

mason ls [prefix] · info · url s/<slug>

List shares, show full detail, or print a URL (short by default).

M Manage & operate

mason mv · cp <path> <path>

Re-home an object path, or duplicate it as a new path + slug.

mason disable · enable s/<slug>

404 an endpoint, reversibly — then bring it back.

mason rm s/<slug> · prune [-y]

Remove an endpoint, or GC orphaned blobs (dry-run unless -y).

mason target ls · add · use · rm

Manage named destinations and pick the default.

mason setup · remote-init · doctor · sync

Onboard, bootstrap the server, health-check, and refresh the local cache.

Global flags

-t, --target
Select a named target for this command.
--json
Emit structured output for parsing.
-n, --dry-run
Show the plan; change nothing.
-y, --yes
Skip confirmation prompts.

Requirements

local
Go (to build), ssh, rsync.
remote
SSH access, rsync, caddy. Bootstrapped via remote-init.
Full setup in the README

Build it on GitHub.

Mason is a single Go binary, MIT-licensed, and small enough to read end-to-end. Clone it, build it, and point it at your own VPS.