- Go 63.6%
- JavaScript 16%
- Shell 9.7%
- CSS 8.4%
- HTML 2.1%
- Other 0.2%
v0.3.0 dropped `reload:` and unknown keys are fatal, so updating the binary on a v0.2.x install restarts a service that will not come back up. The docs said "just run ctconf -update", which is now a trap. |
||
|---|---|---|
| .forgejo/workflows | ||
| cmd/ctconf | ||
| deploy | ||
| docs | ||
| internal | ||
| scripts | ||
| web | ||
| .gitignore | ||
| AGENTS.md | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
| SPEC.md | ||
ctconf
Edit config files inside your LXC from a phone and apply them with a button — without SSH, nano, and prayer.
ctconf is a single-binary web tool you install inside an existing Proxmox
LXC (or any systemd Linux). You register the text files it manages (a
Caddyfile, a filebrowser config, anything), edit them in a real code editor in
your browser, and press buttons you defined in YAML — caddy reload, a
reset, a reboot-safe restart, ctconf -update — with the output streamed
straight back to you.
Why
Editing /etc/caddy/Caddyfile over SSH on a phone is counter-intuitive and one
typo away from a broken web server. oliveTin runs commands but has no
first-class "edit this file, then apply it" workflow. ctconf is that workflow:
file-centric, with lean safety (atomic writes, version history, restore) and
explicit buttons instead of magic.
⚠️ Security — read this first
ctconf has no authentication. Anyone who can reach its port can edit the registered files and run their configured commands as root. That is remote code execution by design, and it is only acceptable on a trusted LAN. If your LAN is not trusted, put ctconf behind something that authenticates (reverse proxy with auth, Tailscale, VPN) before exposing it.
Install
Setting up from scratch? docs/SETUP.md walks through testing on a scratch LXC (creation, manual systemd setup), the full config reference, day-2 operations and troubleshooting.
SSH into the target container and run:
wget -qO- https://git.geertrademakers.nl/master/ctconf/raw/branch/main/scripts/install.sh | bash
The installer (run as root inside the container):
- drops the static binary at
/usr/local/bin/ctconf, - writes a
ctconf.servicesystemd unit and starts it, - creates
/etc/ctconf/config.yamlif absent — registering ctconf's own config first (so you can add the rest of your files from the browser instead of over SSH), plus/etc/caddy/Caddyfilewhen it exists, and two instance-wide buttons: ⬆️ Update ctconf and ♻️ Restart. It never overwrites an existing config.
Then open http://<container-ip>:8090 from your phone or laptop.
Updating
The binary updates itself — from the UI as ⬆️ Update ctconf in the header, or by hand:
ctconf -update # fetch the newest release, swap the binary, restart
ctconf -version # what am I running?
It downloads the release asset for this architecture, refuses anything that isn't a Linux binary, swaps it in atomically and restarts the service. If the running version is already the newest release it does nothing.
Re-running the installer does the same job and also refreshes the systemd unit; your config and version history survive either way.
Coming from before v0.3.0: that release removed the reload: config key, and
unknown keys are fatal, so update the config (→ actions:) before updating the
binary or the service will refuse to start. See
docs/SETUP.md §B6.
Configuration
Buttons are config
Every button in ctconf comes from actions: in your YAML — ctconf has no idea
what a "reload" is. Top-level actions are instance-wide (they render in
the header); files[].actions render next to Save.
listen: 0.0.0.0:8090
data_dir: /var/lib/ctconf
versions_kept: 10
actions:
- id: update # optional; defaults to a slug of the title
title: Update ctconf # required
icon: "⬆️" # optional emoji
cmds: [ctconf -update] # required, run in order via sh -c
confirm: true # false | true | "your own question"
restarts_ctconf: true # UI waits for the server and reloads itself
- id: restart
title: Restart
icon: "♻️"
cmds:
- ctconf -check-config
- systemctl --no-block restart ctconf # --no-block: stream first, die after
style: danger
confirm: true
restarts_ctconf: true
files:
- name: Caddyfile
path: /etc/caddy/Caddyfile
language: caddyfile # optional; inferred from extension
actions:
- id: reload
title: Reload
icon: "🔄"
cmds:
- caddy validate --config /etc/caddy/Caddyfile
- caddy reload --config /etc/caddy/Caddyfile
timeout: 120s # optional, per command
- id: reset
title: Reset
icon: "🧹"
cmds: [systemctl restart caddy]
confirm: true
style: danger
nameis the unique id used in the UI and API;idis unique per scope and is what run history is recorded under (give your actions one).- Commands run in order as root via
sh -c; a non-zero exit stops the sequence and is reported as a failed run. - A file with no
actionslist is edit-only. - Anything invalid — duplicate ids, blank
cmds, the oldreload:key — is a startup error naming the file and action, never a silent skip. Check before applying:ctconf -check-config.
ctconf's own config registers an Apply config button that validates before restarting:
- name: ctconf
path: /etc/ctconf/config.yaml
language: yaml
actions:
- id: apply
title: Apply config
icon: "♻️"
cmds:
- ctconf -check-config -config /etc/ctconf/config.yaml
- systemctl --no-block restart ctconf
style: danger
restarts_ctconf: true
confirm: "Apply this config to ctconf and restart it?"
Edit, Save, Apply config — a broken config fails the action with the error in the console and leaves the running server on its old config, rather than bricking the UI you would need to fix it. The config is never hot-reloaded, so the restart is what applies it.
How it behaves
- Save writes atomically (temp + rename, preserving mode/owner) and snapshots the previous content into version history. It never runs anything.
- Actions stream output live; each command has a timeout (default 120 s) and is killed with its whole process group. One run at a time per file — a second button on a busy file is refused — while different files can run at once.
- Status badge per file: green up to date (disk matches what ctconf last wrote), red modified externally (someone edited over SSH; the editor shows current disk content so saving is an explicit overwrite, and Load from disk accepts it as the new baseline), grey missing, amber unsaved changes while your buffer differs.
- Action dots answer the question the file status cannot: did this command run against these bytes? Green = yes, amber = the file changed since, red = last run failed, no dot = never run.
- History drawer per file: restore any snapshot (with confirm). Restoring counts as a save, so the actions that ran against the old bytes go amber.
Development
See AGENTS.md for architecture and testing guidance; design decisions live in SPEC.md.
make build # dist/ctconf
make test # go test ./...
make smoke # end-to-end: real server on 127.0.0.1:18090, curl assertions
Roadmap (not in v1)
- Pre-save validation commands + auto-rollback on a failed action.
- Save groups with ordered multi-file action sequences.
- Optional auth / token gate for non-LAN use.