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. A project needs two lines of config — its URL and its build folder — and Mason works out the rest. No daemon runs on the server; the remote is just SSH, rsync, and Caddy.

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

~/sites/blog.me.dev  —  mason
# deploy this project's build folder to its site mason deploy Site: https://blog.me.dev → me.dev:/var/www/blog.me.dev Build: dist Plan: 3 new · 1 changed · 8 unchanged (2.4 MB to send) ✓ Done in 5.1s — 4 files updated, 2.4 MB sent # share a file and get a short link back mason share ./talk.mp3 Sharing 'talk.mp3' → inbox/talk.mp3 Uploaded 4.1 MB. Long: https://files.me.dev/inbox/talk.mp3 Short: https://files.me.dev/s/f76Ab1.mp3▍

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, the vhost. 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.

Your server is named once, in ~/.mason. Each project's .mason says only which site it is and what to deploy. When the server changes, you fix it in one place.

Configure once

Two lines per project

A project names its URL and its build folder. The server comes from ~/.mason; the path on the server, the vhost, and the SSH details are derived. Mason finds the project file from any subfolder.

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 directory layout and Mason-managed Caddy snippets — nothing to keep alive, nothing to patch.

Plain & scriptable

Readable by humans and machines

Output is columnar and greppable; add --json for the machine-readable counterpart. mason status shows a deploy's plan, and --print shows any server script, 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

mason deploy syncs the project's build folder to its site, showing the plan, live progress, and a summary. Use --delete to mirror exactly — removals are confirmed before they happen.

  • mason provision stands up a new site — web root and Caddy vhost, apex or subdomain auto-detected — in one idempotent step.
  • Environments: list staging and live in one project, then mason deploy live.
  • Dotfiles — .git, .env, .mason — are never deployed, and every vhost 404s them anyway.

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 a readable path and a short slug — s/<slug> — controlled independently.
  • Content-addressed blobs: re-sharing identical bytes links, never re-uploads.
  • One share store per machine, so a share can never land inside a website.

One-command setup

mason setup <server> uses the SSH you already have, or creates a key and bootstraps the server — after asking. Re-run it anytime.

Careful with your server

Every Caddy change is validated, with the previous config restored if it fails. Vhosts you edited by hand are never overwritten without --force.

Self-checking

mason doctor checks what you've configured — the server, your share store, the current site — and says what to fix.

Quickstart

Set up once, then publish.

One command for this machine, one for each new site, then deploy whenever you build. Every step is safe to re-run.

01

Set up this machine

mason setup reuses the SSH setup you already have when it works. Otherwise it creates ~/.ssh/mason and, after asking, bootstraps the server as root: an unprivileged mason user, this machine's key, and one Caddy import line. Add --print to read the script first.

setup
mason setup me.dev Generating ~/.ssh/mason for this machine… Bootstrap me.dev as root? This creates the "mason" user, authorizes this machine's key, and adds Caddy's import line (review with --print). [y/N] y Bootstrapping me.dev as root@me.dev… ✓ mason@me.dev is reachable ✓ Set up: this machine publishes to mason@me.dev key: /home/you/.ssh/mason config: /home/you/.mason
02

Add a site

In the project folder, mason init suggests the URL from the folder name and finds the build folder, writes a two-line .mason, then offers to provision the site: its web root and a Caddy vhost. Apex domains also get a permanent www → apex redirect.

~/sites/blog.me.dev
mason init Site URL [https://blog.me.dev]: Build folder [dist]: Created /home/you/sites/blog.me.dev/.mason Provision https://blog.me.dev on me.dev as root? Creates /var/www/blog.me.dev and /etc/caddy/mason.d/blog.me.dev.caddy, then reloads Caddy. [y/N] y ✓ Provisioned https://blog.me.dev Next: mason deploy
03

Deploy

Preview with mason status, then mason deploy. It works from any folder in the project. A project with several environments deploys the first by default — mason deploy live picks another.

deploy
mason status Site: https://blog.me.dev → me.dev:/var/www/blog.me.dev Build: dist Plan: 9 new · 0 changed · 0 unchanged (1.2 MB to send) mason deploy … ✓ Done in 1.8s — 9 files updated, 1.2 MB sent
04

Share a file

Create your share store once with setup --share. Then drop a file on the server and get a link — use --as to choose its path.

share
mason setup me.dev --share https://files.me.dev mason share ./talk.mp3 --as podcasts/ep-01.mp3 Sharing 'talk.mp3' → podcasts/ep-01.mp3 Uploaded 8.8 MB. Long: https://files.me.dev/podcasts/ep-01.mp3 Short: https://files.me.dev/s/a14Xe2.mp3

Another laptop?

Run mason setup me.dev on it. If it has no admin access, setup prints a one-liner — mason key add 'ssh-ed25519 AAAA… mason@laptop2' — to run on a machine that already publishes. mason key ls shows every authorized machine; mason key rm revokes one.

Configuration

Four lines, usually.

~/.mason says where you publish. A project's .mason says what it is. Mason merges the two — and you rarely write either by hand: setup and init do it.

This machine

~/.mason
server: me.dev share: https://files.me.dev

A project

~/sites/blog.me.dev/.mason
site: https://blog.me.dev build: dist

With environments

~/sites/shop/.mason
build: dist sites: staging: https://shop.me.dev live: https://shop.com

What Mason derives

path
/var/www/<host> on the server
vhost
/etc/caddy/mason.d/<host>.caddy
ssh
user mason; port, key, and host keys from ~/.ssh/config and your agent
name
its key under sites:, or its host

Override only what differs

A site can set its own path, server, or spa: true. Typos get a "did you mean", and old v1 files point straight to the migration guide.

Full reference: CONFIG.md

Core concepts

A small model, deliberately.

A server says where. Sites and shares say what. The server contract ties it together.

Concept · 1

Servers & sites

A server is just an SSH destination. A site is a URL plus the folder that builds it, and its host is its identity — the vhost, the server path, and its name all follow from it.

Concept · 2

Shares are links

A share is a link to a content-addressed blob, not a copy. Two endpoints address it independently — a readable 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 Mason-managed Caddy snippets, marked # managed by mason. The share store 404s /objects/* and /.mason/*; sites 404 every dotfile.

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 — it only reports until you add -y.

CLI reference

The whole surface, on one page.

Add --json to any command for machine-readable output. Commands that change the server ask first; -y answers yes.

P Publish

mason init [url] [--build dir] [--no-provision]

Write the project's .mason (URL + build folder), then offer to provision.

mason provision [site] [--spa] [--print] [--force]

Create the site's web root and Caddy vhost — apex or subdomain, auto-detected. Validates, rolls back on failure, never overwrites a hand-edited vhost without --force.

mason status [site]

Show the deploy plan — no transfer.

mason deploy [site] [--delete] [-n]

Sync the build folder: plan → progress → summary. --delete mirrors exactly (confirmed).

mason share <file> [--as path]

Upload a file; print its long and short URLs.

mason ls [prefix] · info · url <ref>

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

M Manage & operate

mason setup <server> [--share url] [--print]

Set up this machine; bootstrap the server and share store as needed.

mason key ls · add · rm

See, authorize, and revoke the machines that can publish.

mason mv · cp <ref> <path>

Re-home a share's path, or duplicate it as a new path + slug.

mason disable · enable · rm <ref>

404 one endpoint reversibly, bring it back, or remove it for good.

mason prune [-y] · sync

Garbage-collect orphaned blobs (reports unless -y); refresh the local cache.

mason doctor [site] · version

Check the server, share store, and site; print the build you're running.

Global flags

--json
Emit structured output for parsing.
-y, --yes
Answer yes to confirmations.
-q · -v
Quieter, or more detail.
--config
Use a specific project .mason.

Requirements

local
Go (to build), ssh, rsync.
remote
rsync, caddy, and admin SSH once — mason setup does the rest.
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.