New service for one narrow job — getting SMS verification codes sent to a
VoIP number onto a phone with no SIM. Deliberately not a texting app: no
outbound path (Anveo Direct has none; that needs an Anveo Retail account, and
a free texting app covers sending), and messages arrive as push notifications
rather than being routed into Asterisk as SIP MESSAGE, since a code you read
and type is better served by a notification than a softphone chat thread.
Two modes, both driven entirely from the provider's "forward SMS to URL" box:
- direct — the provider calls ntfy itself; nothing installed here. ntfy
accepts GET publishing at /{topic}/(publish|send|trigger) with message and
title as query params, and auth via ?auth= holding base64url (unpadded) of
the literal "Bearer <token>" — confirmed against ntfy's server.go and
server_auth.go rather than its docs.
- relay — a stdlib systemd service, Caddy-fronted on its own domain with no
Authelia (the provider can't log in; a random 32-char token in the path is
the secret). Buys two things direct mode can't have: an unescaped "&" in a
message body survives intact, because the relay takes everything after the
last message= verbatim instead of parse_qs — which is why the generated URL
always puts the message placeholder last — and no ntfy credentials sit in a
third party's web portal.
Verification codes are bearer credentials, so: a 24-char random topic name
(the repo's ntfy defaults to auth-default-access: read-write, making the topic
name the read credential), constant-time token compare, a 60/min rate limit,
and the relay logs sender/recipient/length but never the message body.
The Anveo guide gains a section covering the two things that actually decide
whether codes arrive: short-code support (Anveo has it, unusually — VoIP.ms
does not except for Google) and Anveo's carrier-sourced *mobile* DIDs, which
are classified as mobile in the lookups that reject VoIP numbers at signup.
Also documents MMS and group texts being out of reach, and why the native
Messages app never sees any of this.
Verified against a stub ntfy: plain OTP, encoded "&", unencoded "&", "+" as
space, wrong token (404), missing message (400) and the rate limit (57x204
then 429) all behave; both installer modes were run end to end in a sandbox
and their generated URLs, settings files and READMEs checked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh
374 lines
14 KiB
Markdown
374 lines
14 KiB
Markdown
# ubuntu-post-install
|
|
|
|
Modular post-install system for Ubuntu servers. One repo, one entry point,
|
|
install only what you need — interactively or by name.
|
|
|
|
## Quick start on a fresh box
|
|
|
|
**Public repo — paste on any new box:**
|
|
```bash
|
|
curl -fsSL https://raw.githubusercontent.com/outis1one/ubuntu-post-install/main/bootstrap.sh | sudo bash
|
|
```
|
|
|
|
**USB thumb drive — works for public or private repos:**
|
|
|
|
Prepare the USB once on any machine (no git required):
|
|
1. Go to the repo on GitHub → green **Code** button → **Download ZIP**
|
|
2. Unzip it — you'll get a folder called `ubuntu-post-install-main`
|
|
3. Copy that folder to your USB drive
|
|
|
|
On every new box:
|
|
1. Plug in the USB — it opens in the file manager
|
|
2. Navigate into the `ubuntu-post-install-main` folder
|
|
3. **Either:**
|
|
- Right-click inside the folder → **Open in Terminal** → type `bash bootstrap.sh`
|
|
- **Or** double-click `bootstrap.sh` → if prompted, choose **Run in Terminal**
|
|
|
|
The script asks for your password if needed. It detects it is running from
|
|
inside the repo, copies everything to `~/ubuntu-post-install`, then launches
|
|
the wizard — the USB can be unplugged once setup starts.
|
|
|
|
**Private repo — PAT (alternative):**
|
|
```bash
|
|
sudo bash bootstrap.sh --pat ghp_xxxxxxxxxxxxxxxxxxxx
|
|
```
|
|
Use a fine-grained read-only PAT scoped to just this repo (Contents: Read).
|
|
The PAT is stripped from the stored remote URL after cloning.
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
sudo ./setup.sh # interactive wizard
|
|
sudo ./setup.sh caddy immich # install specific services
|
|
sudo ./setup.sh configure # set site defaults (timezone, domain, Caddy network)
|
|
./setup.sh --list # list all services grouped by category
|
|
sudo ./setup.sh --dry-run immich # preview without making changes
|
|
sudo ./setup.sh --unattended base # non-interactive, use defaults
|
|
```
|
|
|
|
## What the wizard does
|
|
|
|
**First run:**
|
|
1. Installs essential CLI packages (`net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`, `glow`), Docker CE + Compose plugin, and `openssh-server` — offers to import SSH keys from GitHub/Launchpad (`ssh-import-id`), disable password login once a key is confirmed, install NetBird, and add SSH Host aliases (see [SSH Host aliases](#ssh-host-aliases))
|
|
2. Asks **where Caddy runs** — this machine, a remote machine/VPN peer, or none — before anything else, since every later service prompt depends on the answer
|
|
3. If Caddy is local: offers to set **site defaults** — timezone, base domain, Caddy Docker network — so every service picks them up automatically instead of asking each time, then offers to install Caddy itself
|
|
4. Drops into a **category menu** — pick a group, tick services, install, repeat
|
|
5. Ends by dropping you into a fresh login shell so the `docker` group takes effect immediately (no manual `newgrp docker` or SSH reconnect needed)
|
|
|
|
**Re-run:** skips steps already completed, shows a summary of installed services, and goes straight to the menu.
|
|
|
|
**Site defaults** are saved to `~/docker/.config` and pre-fill every service prompt.
|
|
Update them any time with `sudo ./setup.sh configure`. Remote/none Caddy mode
|
|
skips the domain/timezone prompt entirely — service installers instead save
|
|
a ready-to-copy Caddy config snippet to `~/docker/caddy-snippets/`.
|
|
|
|
## Services
|
|
|
|
| Group | Services |
|
|
|-------|---------|
|
|
| `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network |
|
|
| `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine` |
|
|
| `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` |
|
|
| `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` |
|
|
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
|
|
| `gaming` | `drum-rhythm-game`, `js99er`, `kyber-launcher`, `kyber-server`, `minecraft`, `wolf`, `wolf-pair` |
|
|
| `extras` | `kdeconnect`, `silent-send`, `ssh-config`, `sync-cc` |
|
|
| `backup` | `backup` — complete recovery: entire `~/docker/<service>/` for every service via Kopia (Minecraft: flush+snap, no downtime; others: stop/snap/start for DB consistency); `borg-backup` — same coverage via Borg (chunk dedup, SSH remote repos, Borgmatic/Vorta compatible); `gaming-backup` — frequent game-save snapshots (Minecraft world data, emulator saves, Steam — no downtime, run hourly) |
|
|
|
|
Run `./setup.sh --list` to see descriptions.
|
|
|
|
<details>
|
|
<summary>Copiable list of all services by category</summary>
|
|
|
|
```
|
|
base
|
|
base
|
|
glow
|
|
|
|
homelab
|
|
caddy
|
|
crowdsec
|
|
authelia
|
|
homeassistant
|
|
asterisk
|
|
pstn-trunk
|
|
sms-inbound
|
|
security-dashboard
|
|
sunshine
|
|
|
|
utilities
|
|
actualbudget
|
|
ai-gpu
|
|
ai-stack
|
|
archivebox
|
|
changedetection
|
|
ddclient
|
|
filebrowser
|
|
fmd
|
|
gatus
|
|
homebox
|
|
iopaint
|
|
joplin
|
|
koha
|
|
magicmirror
|
|
mail-archiver
|
|
mattermost
|
|
mealie
|
|
meshcentral
|
|
n8n
|
|
nextcloud
|
|
ntfy
|
|
onlyoffice
|
|
paintplus
|
|
portainer
|
|
rustdesk
|
|
stirling-pdf
|
|
syncthing
|
|
traccar
|
|
unifi
|
|
uptimekuma
|
|
vaultwarden
|
|
watchyourlan
|
|
watchtower
|
|
wg-easy
|
|
|
|
media
|
|
arm
|
|
audiobookshelf
|
|
calibre-web
|
|
emby
|
|
immich
|
|
jellyfin
|
|
lyrion
|
|
|
|
cameras
|
|
frigate
|
|
frigate-audio
|
|
frigate-notify
|
|
sky-cam
|
|
|
|
gaming
|
|
drum-rhythm-game
|
|
js99er
|
|
kyber-launcher
|
|
kyber-server
|
|
minecraft
|
|
wolf
|
|
wolf-pair
|
|
|
|
extras
|
|
kdeconnect
|
|
silent-send
|
|
ssh-config
|
|
sync-cc
|
|
|
|
backup
|
|
backup
|
|
borg-backup
|
|
gaming-backup
|
|
```
|
|
|
|
</details>
|
|
|
|
## Layout
|
|
|
|
```
|
|
setup.sh dispatcher — wizard, direct install, --list, --dry-run
|
|
lib/common.sh shared helpers: logging, prompts, site config, OS detection
|
|
services/ one file per service (self-registering)
|
|
extras/ non-Docker assets bundled with the repo (e.g. sync_cc.py)
|
|
CLAUDE.md contributor guide — how to add services, available helpers
|
|
```
|
|
|
|
## Managing installed services
|
|
|
|
Every Docker service installs to its own `~/docker/<name>/` folder:
|
|
|
|
```bash
|
|
cd ~/docker/immich
|
|
docker compose up -d # start
|
|
docker compose logs -f # logs
|
|
docker compose pull && docker compose up -d # update
|
|
docker compose down # stop
|
|
```
|
|
|
|
## SSH Host aliases
|
|
|
|
`~/.ssh/config` lets you `ssh <alias>` instead of typing `ssh user@1.2.3.4`
|
|
every time — especially handy once machines are reachable over a VPN/NetBird
|
|
overlay network where the IP is easy to forget:
|
|
|
|
```
|
|
Host myserver
|
|
HostName 100.x.x.x
|
|
User someuser
|
|
Port 22
|
|
```
|
|
|
|
Three ways to manage these entries:
|
|
|
|
- **During `base` install** — after SSH key import, the wizard offers to add
|
|
one or more aliases interactively
|
|
- **Any time** — `sudo ./setup.sh ssh-config` lists, adds, or removes aliases
|
|
without touching anything else
|
|
- **By hand** — edit `~/.ssh/config` directly; it's a plain OpenSSH client
|
|
config file, nothing generated or templated beyond the `Host` block itself
|
|
|
|
Aliases are written to the invoking user's own config (not root's), since
|
|
that's whose terminal actually runs `ssh`.
|
|
|
|
## Installing from a USB thumb drive
|
|
|
|
No git required. Works for anyone with a browser.
|
|
|
|
### 1 — Put the repo on the USB
|
|
|
|
1. On GitHub: click **Code → Download ZIP**
|
|
2. Open your Downloads folder — right-click the ZIP → **Extract Here**
|
|
3. Drag the `ubuntu-post-install-main` folder onto the USB drive in the
|
|
file manager sidebar
|
|
|
|
To update later: download the ZIP again, extract, drag the new folder to the
|
|
USB and replace the old one.
|
|
|
|
### 2 — Run on the target machine
|
|
|
|
Plug in the USB. Open the `ubuntu-post-install-main` folder in the file manager,
|
|
then either:
|
|
|
|
- **Right-click inside the folder → Open in Terminal**, then run:
|
|
```bash
|
|
sudo bash bootstrap.sh
|
|
```
|
|
|
|
- **Double-click `bootstrap.sh`** → click *Run in Terminal* → it prompts for
|
|
your sudo password and starts the wizard automatically.
|
|
|
|
### Notes
|
|
|
|
- Everything the wizard installs goes to `~/docker/` on the **target machine's
|
|
disk** — only the setup scripts live on the USB.
|
|
- **exFAT** is the best filesystem for the USB — readable on Windows and macOS
|
|
for easy ZIP extraction, and works fine on Linux.
|
|
|
|
## Compatibility
|
|
|
|
Tested on **Ubuntu 24.04 LTS** and **26.04 LTS**.
|
|
Works on any Ubuntu LTS ≥ 22.04; non-LTS releases also work.
|
|
The wizard shows the detected OS in the header and warns on unknown versions.
|
|
|
|
## Gaming scripts
|
|
|
|
Standalone scripts in `scripts/` for gaming setup — not part of the main
|
|
wizard, run separately.
|
|
|
|
### Star Wars Battlefront II (2017) + Kyber
|
|
|
|
**`scripts/setup-swbf2-linux.sh`** — Configure SWBF2 on native Linux Steam
|
|
(Proton, controller, performance tweaks).
|
|
|
|
**`scripts/setup-kyber-linux.sh`** — Install the native Linux Kyber launcher.
|
|
|
|
Kyber is the community multiplayer replacement for SWBF2 after EA shut down
|
|
official servers in 2022. It went open-source (GPL) in January 2026.
|
|
|
|
**The correct approach is a native Linux AppImage** — not Wine or Proton for
|
|
the launcher itself. The AppImage is maintained at:
|
|
https://github.com/simonlinuxcraft/kyber-linuxport-unofficial
|
|
|
|
```bash
|
|
chmod +x scripts/setup-kyber-linux.sh
|
|
./scripts/setup-kyber-linux.sh
|
|
```
|
|
|
|
The script downloads the latest AppImage, installs a desktop entry, and
|
|
creates a `kyber` command in `~/.local/bin`.
|
|
|
|
**Every time you want to play:**
|
|
1. Open **Steam** (must be running for library validation) — do NOT click Play on SWBF2
|
|
2. Launch **Kyber** (`kyber` or from the app menu)
|
|
3. In Kyber: join a server (HOME) or create one (HOST)
|
|
4. Kyber/Maxima launches SWBF2 via its own bundled GE-Proton — wait 1-3 minutes
|
|
5. If the SWBF2 window appears but won't focus: press **Alt+Tab** or click its
|
|
taskbar entry — this is normal when the game is launched by a wrapper process
|
|
|
|
Do NOT launch SWBF2 from Steam directly. If Steam's SWBF2 is already running
|
|
when Kyber starts, kill it first — Kyber cannot inject into a Steam-launched instance.
|
|
|
|
**If Kyber says "Game Not Found":**
|
|
Click **SET GAME FOLDER** and point it to the SWBF2 install directory.
|
|
Find it with:
|
|
```bash
|
|
find ~/.steam/steam/steamapps -name "starwarsbattlefrontii.exe" 2>/dev/null | head -1 | xargs dirname
|
|
```
|
|
Paste that path into the SET GAME FOLDER dialog.
|
|
|
|
**If Origin Error: "title installed in language not entitled to play":**
|
|
Maxima's Wine prefix is missing locale registry keys — its setup commands fail silently on some systems. Fix:
|
|
```bash
|
|
cat > /tmp/swbf2_fix.reg << 'EOF'
|
|
Windows Registry Editor Version 5.00
|
|
[HKEY_LOCAL_MACHINE\Software\Origin Games\1035052]
|
|
"locale"="en_US"
|
|
"displayname"="STAR WARS Battlefront II"
|
|
[HKEY_LOCAL_MACHINE\Software\Wow6432Node\Origin Games\1035052]
|
|
"locale"="en_US"
|
|
"displayname"="STAR WARS Battlefront II"
|
|
[HKEY_LOCAL_MACHINE\Software\Electronic Arts\EA Desktop]
|
|
"InstallSuccessful"="true"
|
|
[HKEY_LOCAL_MACHINE\Software\Origin]
|
|
"InstallSuccessful"="true"
|
|
"ClientPath"="C:\\Windows\\System32\\conhost.exe"
|
|
[HKEY_CURRENT_USER\Control Panel\International]
|
|
"Locale"="00000409"
|
|
"LocaleName"="en-US"
|
|
"sLanguage"="ENU"
|
|
EOF
|
|
WINEPREFIX=~/.local/share/maxima/wine/prefix wine64 regedit /tmp/swbf2_fix.reg
|
|
```
|
|
Then restart Kyber and try again.
|
|
|
|
**First run (one-time setup):**
|
|
1. Click **EA Account** → log in with your EA credentials in the browser
|
|
2. Click **Skip** on Nexus Mods (optional, only needed for mods)
|
|
3. EA login is cached — you stay logged in across sessions
|
|
|
|
**Hosting a private server with bots:**
|
|
- HOST → pick maps/modes → set a **name** and **PASSWORD** → Start Server
|
|
- Share the server name + password with friends; they search by name in HOME
|
|
- Bot count: in the HOST panel right side → **AUTOPLAYERS** section →
|
|
set **BOTS TEAM 1** and **BOTS TEAM 2** (e.g. 4 each) → click **UPDATE SERVER**
|
|
- Bot difficulty: the **BOT DIFFICULTY** slider (RECRUIT → OFFICER → KNIGHT → MASTER)
|
|
- After the game loads you can also update settings live and hit UPDATE SERVER again
|
|
|
|
**Requirements:**
|
|
- SWBF2 (Steam AppID 1237950) installed via Steam
|
|
(Kyber manages its own GE-Proton for launching the game)
|
|
- glibc 2.38+ — Ubuntu 24.04+, Fedora 38+, SteamOS 3.7+
|
|
- EA account (free) at ea.com
|
|
- Unprivileged user namespaces enabled (Ubuntu 24.04 restricts these by default):
|
|
```bash
|
|
sudo sysctl -w kernel.unprivileged_userns_clone=1
|
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
```
|
|
The setup script applies this automatically when run with sudo and saves it
|
|
to `/etc/sysctl.d/99-userns.conf` to persist across reboots.
|
|
Without this fix Kyber fails with: `bwrap: setting up uid map: Permission denied`
|
|
|
|
**If SWBF2 crashes immediately when a level starts loading:**
|
|
Likely a DXVK rendering issue, especially on integrated GPUs (Intel Iris Xe, etc.).
|
|
Disable fullscreen and HDR in the game settings file — the game writes this on first run:
|
|
```bash
|
|
PROFILE=$(find ~/.local/share/maxima -name "ProfileOptions_profile" 2>/dev/null | head -1)
|
|
sed -i 's/GstRender.FullscreenEnabled 1/GstRender.FullscreenEnabled 0/' "$PROFILE"
|
|
sed -i 's/GstRender.EnableHDR 1/GstRender.EnableHDR 0/' "$PROFILE"
|
|
```
|
|
Then restart Kyber and try hosting/joining again.
|
|
|
|
**What does NOT work:**
|
|
- Running the Windows `kyber_launcher.exe` under Wine/Proton: EA's auth
|
|
callback uses the `eadesktop://` URI scheme which has no Linux handler,
|
|
and Wine's cmd.exe crashes on long OAuth URLs anyway
|
|
- Running Kyber inside Wolf/Games-on-Whales: the Docker double-sandbox
|
|
blocks the user namespace clone that Proton requires
|