Skip to content

Commit dae47bc

Browse files
authored
Add root-ubuntu.sh: provision an Ubuntu/Debian server from one file (#24)
1 parent 1fca2bf commit dae47bc

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)