Skip to content

Commit bf36321

Browse files
ralyodioclaude
andcommitted
Add root-ubuntu.sh: provision an Ubuntu/Debian server from one file
Moves the server provisioner out of the private dotfiles repo and into cli-tools, where it can be curled onto a box that has nothing on it yet. It is the odd one out here -- bash rather than TypeScript, and not linked onto PATH -- because it has to run before Node exists. Three things had to change for it to live in a public repo and work on a machine that is not one of ours. Dotfiles are now optional and are not in this repository. They cannot be: a dotfiles tree carries ssh config, known_hosts and sometimes keys. The script used to die unless it was sitting inside that checkout, which is exactly what stopped it running anywhere else. It now resolves a source in order -- DOTFILES_DIR, the directory holding the script, DOTFILES_REPO cloned to a cache, or nothing -- and skips only the dotfile stages when there is none. install_ssh had to be split for this: authorising the account's own key is not a dotfiles job, and an earlier cut of this returned early without one, so accounts got created that could not log in. Nothing identifying anybody is left in it. ACME_EMAIL has no default, because a made-up address sends a stranger's certificate warnings into a black hole, and certbot is passed --register-unsafely-without-email rather than an empty -m. There is no default ad slot: a slot id is an account, so shipping one bills every box that ever runs this to whoever owns it. Credentials come from the environment or the config file only. Configuration is read, not sourced. The environment has to win over the file, and `.` assigns unconditionally, so a sourced config would quietly beat the value someone just put on the command line -- and this runs as root, where sourcing hands a config file the whole machine. It is KEY=value, parsed, with nothing executed. Not JSON, because the script runs before apt has installed jq. Also: a POSIX guard that says so in one sentence when the script is run under sh, since /bin/sh on Ubuntu is dash and the old one-liner would have failed on the first [[ with a syntax error naming a line nobody typed; an OS check that warns rather than refuses; --groups, so accounts can be provisioned from a pipe; and the landing page still recognises the marker from when this lived in the dotfiles repo, without which every already-provisioned box would decide its own page was hand-edited and never touch it again. 34 tests cover the pure helpers by cutting them out of the file and running them in a real bash, plus the invariants that make it safe to publish: no personal identifiers, no key material, no default slot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1fca2bf commit bf36321

5 files changed

Lines changed: 4438 additions & 1 deletion

File tree

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ TypeScript, installed as executables on `PATH`.
2121
| [`vid`](#vid) | Inspect, thumbnail, clip and shrink video, through ffmpeg |
2222
| [`codeburn`](#codeburn) | See where your AI spend goes, by task, tool, model and project |
2323

24+
One thing here is not a `PATH` command and does not need Node:
25+
26+
| Script | What it does |
27+
| --- | --- |
28+
| [`root-ubuntu.sh`](#root-ubuntush) | Provision an Ubuntu/Debian server: dev environment, accounts, web, TLS |
29+
2430
## Requirements
2531

2632
- **Node 20+**
@@ -683,6 +689,83 @@ before every launch, which is fine for a one-shot and wrong for a dashboard you
683689
open twenty times a day. Upstream wants **Node 22.13+**; on an older one it says
684690
so and tries anyway, since that floor is theirs to move.
685691

692+
### `root-ubuntu.sh`
693+
694+
Sets up a server the way we like them, and keeps it that way. It is the odd one
695+
out in this repository: a single bash script rather than a TypeScript command,
696+
because it has to run on a machine where nothing is installed yet — including
697+
Node. Nothing links it onto `PATH`; you curl it onto the box.
698+
699+
```sh
700+
# on the server, as root
701+
curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/root-ubuntu.sh \
702+
| bash -s -- --refresh
703+
```
704+
705+
**`bash`, not `sh`.** `/bin/sh` on Ubuntu is dash and this script is bash
706+
throughout. Piping it into `sh` stops with one sentence telling you so rather
707+
than a syntax error on a line you never typed.
708+
709+
A pipe has no terminal on stdin, so that form is always non-interactive: it
710+
takes defaults instead of reading answers out of its own source. To be asked the
711+
questions, download it first:
712+
713+
```sh
714+
curl -fsSLO https://raw.githubusercontent.com/profullstack/cli-tools/master/root-ubuntu.sh
715+
chmod +x root-ubuntu.sh
716+
./root-ubuntu.sh # as root
717+
./root-ubuntu.sh alice bob # ...and provision two accounts
718+
./root-ubuntu.sh alice --groups sudo,docker
719+
```
720+
721+
What a run does:
722+
723+
- apt update/upgrade, base packages, unattended security updates
724+
- `ufw`, with ssh opened *before* the firewall is enabled
725+
- accounts and groups — created, or refreshed if an earlier run made them
726+
- zsh + oh-my-zsh, oh-my-tmux, mise, moshcode, chawan
727+
- nginx: `~/public_html` at `/~user` and `user.$WEB_DOMAIN`, plus per-user dev
728+
apps at `<app>.<user>.$WEB_DOMAIN`, static or reverse-proxied
729+
- Let's Encrypt, wildcard via DNS-01 where credentials allow it
730+
731+
**Re-running is the update path.** Every step converges rather than assuming a
732+
blank machine: files it owns are rewritten only when the content really changed,
733+
so nginx is not reloaded for nothing; files a user has since edited are never
734+
clobbered, and the new version is left beside them as `.new`; and a lock file
735+
makes two concurrent runs impossible. On a settled box a re-run reports that
736+
nothing changed, which is the point.
737+
738+
#### Configuring it
739+
740+
Read from the environment first, then `$SERVER_CONFIG`, then
741+
`/etc/cli-tools/server.conf`. Copy [`server.conf.example`](server.conf.example),
742+
which documents every value:
743+
744+
```sh
745+
install -d -m 0755 /etc/cli-tools
746+
install -m 0600 server.conf.example /etc/cli-tools/server.conf
747+
```
748+
749+
`KEY=value`, one per line, `#` for comments. The file is **read, not sourced**:
750+
nothing in it executes, so `$(…)` in a config file stays literal text instead of
751+
running as root, and the environment still wins over the file. It is not JSON
752+
either — the script runs before apt has put `jq` on the box, and a bootstrap
753+
that cannot read its own config until it has installed a parser has a hole in
754+
it.
755+
756+
**Dotfiles are optional and are not in this repository.** They cannot be: a
757+
dotfiles tree carries ssh config, `known_hosts` and sometimes keys, and this
758+
repo is public. Point `DOTFILES_REPO` at your own and the script clones it;
759+
leave it unset and the box still gets everything else, with each account keeping
760+
whatever dotfiles it already had. Running the script from inside a dotfiles
761+
checkout also works — it recognises one by its content, not its name.
762+
763+
**No credentials, ever, in the file itself.** `ACME_EMAIL` has no default,
764+
because a public script must not ship somebody's address and a made-up one sends
765+
a stranger's certificate warnings into a black hole. There is no default ad slot
766+
for the same shape of reason: a slot id is an account, so shipping one would bill
767+
every box that ever ran this to whoever owned it.
768+
686769
## As a moshcode plugin
687770

688771
This repo is also a plugin marketplace:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@profullstack/cli-tools",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"private": true,
55
"description": "Local command-line tools, in TypeScript, exposed on PATH.",
66
"type": "module",

0 commit comments

Comments
 (0)