Ship
Ship turns a VPS into a self-hosted git server with a web frontend — similar to running cgit on your own domain. Visiting your base domain in a browser shows a cgit repo index; clicking through shows trees, commit logs, diffs, and blame. Public repos are cloneable over HTTPS. If you host Go code, go get works with your domain out of the box. This is the read-only, public-facing side, and it works exactly the way cgit users expect.
The difference is what happens on the write side. When you git push over SSH, Ship doesn't just update the bare repo — it builds and deploys your code. A post-receive hook checks out the repo, runs docker build, installs a systemd service and Caddy reverse-proxy config, and restarts the app. Your deployment config (the systemd unit, the Caddyfile) lives in .ship/ in your repo and is versioned alongside your code. Push to main and it's live; push to any other branch and nothing happens.
Not every repo needs to be a running service. If there's no Dockerfile, the push is accepted but the deploy step is skipped — the repo just sits there as a browsable, cloneable library. This makes Ship useful for Go modules that only need vanity imports and a public source view, alongside apps that need the full build-and-deploy pipeline. The same base domain serves both.
Ship also supports direct deploys (SCP a binary or rsync a static directory) for cases where git push isn't the right fit.
Ship is a client-side CLI. All state lives on your laptop at ~/.config/ship/state.json. The VPS is configured entirely over SSH — no agent or daemon runs on the server.
Install
go install github.com/bdw/ship/cmd/ship@latest
Or build from source:
go build -o ship ./cmd/ship
Quick start
1. Set up the VPS
ship host init --host user@your-vps --base-domain example.com
This installs Caddy, Docker, git, fcgiwrap, and cgit. It creates a git user for push access, configures sudoers for deploy hooks, and enables automatic HTTPS. The host becomes the default for subsequent commands.
2. Deploy
Git push (Docker-based app):
ship init myapp
git add .ship/ Dockerfile
git commit -m "initial deploy"
git push origin main
Git push (static site):
ship init mysite --static
git add .ship/ index.html
git commit -m "initial deploy"
git push origin main
Git push (library / Go module):
ship init mylib --public
git add .
git commit -m "initial"
git push origin main
No Dockerfile, so nothing is deployed — the repo is just browsable and cloneable at https://example.com/mylib.
Direct (pre-built binary):
GOOS=linux GOARCH=amd64 go build -o myapp
ship --binary ./myapp --domain api.example.com
On first deployment, Ship creates a .ship/ directory in your current working directory containing:
- .ship/service - systemd unit file
- .ship/Caddyfile - Caddy reverse proxy config
These files are uploaded on each deployment. You can edit them locally to customize your deployment (add extra Caddy routes, adjust systemd settings). The systemd service is regenerated when you update resource limits with --memory, --cpu, or --args flags. The Caddyfile is never regenerated, so your custom routes won't be overwritten.
You can version control .ship/ or add it to .gitignore — it's your choice.
Commands
ship init <name>
Create a bare git repo on the VPS and generate local .ship/ config files.
ship init myapp # Docker-based app
ship init mysite --static # static site
ship init myapp --domain custom.example.com # custom domain
ship init mylib --public # publicly cloneable (for go get)
ship deploy <name>
Manually rebuild and deploy a git-deployed app.
ship [deploy flags]
Deploy a pre-built binary or static directory directly.
ship --binary ./myapp --domain api.example.com
ship --binary ./myapp --domain api.example.com --env DB_HOST=localhost
ship --static --dir ./dist --domain example.com
ship --name myapi --memory 512M --cpu 50%
Flags: --binary, --static, --dir, --domain, --name, --env, --env-file, --args, --file, --memory, --cpu
ship list
List all deployments on the default host.
ship status/logs/restart/remove <name>
Manage a deployment's systemd service.
ship env
ship env list myapp
ship env set myapp KEY=VALUE
ship env unset myapp KEY
ship host
ship host init --host user@vps --base-domain example.com
ship host status
ship host update
ship host ssh
ship ui
Launch a local web UI for viewing deployments.
VPS file layout
/srv/git/<name>.git/ # bare git repos
/srv/git/<name>.git/hooks/post-receive # auto-deploy hook
/var/lib/<name>/src/ # checked-out source (for docker build)
/var/lib/<name>/data/ # persistent data volume
/var/www/<name>/ # static site files
/etc/systemd/system/<name>.service # systemd unit
/etc/caddy/sites-enabled/<name>.caddy # per-app Caddy config
/etc/caddy/sites-enabled/ship-code.caddy # base domain Caddy config
/etc/cgitrc # cgit configuration
/etc/ship/env/<name>.env # environment variables
/etc/sudoers.d/ship-git # sudo rules for git user
/opt/ship/vanity/index.html # vanity import template
/home/git/.ssh/authorized_keys # SSH keys for git push
Supported platforms
VPS: Ubuntu 20.04+ or Debian 11+
Security
See SECURITY.md for the threat model, mitigations, and known gaps.
License
MIT
