Removing Linumed Base from a host¶
The orthanc entries below cover a role that v1.0.0 shipped and later releases do not -
it was removed in #92/ADR 0011. They
stay because v1.0.0 is a real tag someone can have deployed, and because the automated
check for this page runs against a host that went through it.
There is no uninstall playbook, and this page is the reason that is a defensible answer rather than a gap. What the kit puts on a host is listed here completely, so removing it is a known quantity - and so that anyone evaluating the kit can see what they would be taking on before they run it.
Nobody should have to guess how to get software off a machine. That question decides whether a service provider tries the kit at all.
Why no uninstall playbook¶
An uninstall role would have to decide what a host looked like before - and it cannot know. Three of the things this kit configures are not its own:
ufw,fail2banandunattended-upgradesmay have been installed already. Removing them because Linumed Base configured them would leave a host less protected than it was found. Thecommonrole deliberately never uninstalls:common_fail2ban_enabled: falsestops and disables the service, it does not purge the package.- SSH hardening is not additive-only in effect. Undoing it means deciding what the previous policy was, which is not recorded anywhere.
- Docker may be carrying the operator's own containers. Removing Docker to undo the
dockerrole could destroy workloads this kit never knew about.
An automated teardown would therefore be guessing on a host that is, by definition, in production. A documented one is slower and safe.
What gets left behind¶
Measured against the roles, not recalled (2026-08-20). Everything this kit creates outside of package installs is prefixed, which is what makes a manual removal tractable.
Files and directories¶
| Path | Role |
|---|---|
/opt/linumed-base/ |
all Compose-based roles - configs, secrets, Compose files |
/etc/ssh/sshd_config.d/10-linumed-hardening.conf |
common |
/etc/ssh/sshd_config.d/90-linumed-tunnel-users.conf |
common (tunnel users) |
/etc/systemd/system/ssh.socket.d/listen.conf |
common (only if the SSH port was changed) |
/etc/fail2ban/jail.d/10-linumed-sshd.conf |
common |
/etc/apt/apt.conf.d/51-linumed-unattended-upgrades |
common |
/etc/apt/apt.conf.d/20auto-upgrades |
common |
/etc/systemd/timesyncd.conf.d/10-linumed.conf |
common |
/etc/apt/keyrings/docker.asc |
docker |
/etc/apt/sources.list.d/docker.list |
docker |
/etc/docker/daemon.json |
docker (log rotation) |
/etc/default/prometheus-node-exporter |
monitoring |
/var/lib/prometheus/node-exporter/ |
monitoring (textfile metrics) |
/usr/local/bin/linumed-base-backup.sh |
backup |
/usr/local/bin/linumed-base-restore-test.sh |
backup |
/etc/systemd/system/linumed-base-backup.{service,timer} |
backup |
/etc/systemd/system/linumed-base-restore-test.{service,timer} |
backup |
Packages installed¶
docker-ce and its plugins, fail2ban, restic, ufw, unattended-upgrades,
prometheus-node-exporter. Any of these may have been present beforehand.
State that is not a file¶
- Docker volumes and images. The volumes hold Prometheus, Loki and Grafana data, BridgeLink's appdata and its PostgreSQL database. This is where the data is - do not remove them before the backup has been verified.
They are not named linumed-base-*. Containers are, because the Compose files set
container_name explicitly; volumes are not, so Compose derives their names from the
project, which is the directory under /opt/linumed-base:
| Role | Volumes |
|---|---|
| caddy | caddy_caddy_data, caddy_caddy_config |
| monitoring | monitoring_prometheus_data, monitoring_loki_data, monitoring_grafana_data, monitoring_alertmanager_data |
| bridgelink | bridgelink_bridgelink_appdata, bridgelink_bridgelink_extensions, bridgelink_bridgelink_db_data |
| orthanc | orthanc_orthanc_storage, orthanc_orthanc_db_data |
This page originally told you to look for them with grep linumed-base, which finds
nothing - so it read as "there is no data here" while every volume was still in place.
The VM test caught it on the first run (issue #68).
- ufw rules added by the roles: the SSH port, Caddy's 80/443, and the Node Exporter
allow/deny pair.
- The restic repository, wherever backup_repository points. Outside the host if it is
remote, and deliberately not touched by anything here.
- Shell-less tunnel users created via common_ssh_tunnel_users.
Removing it, in order¶
Order matters: stopping the backup first prevents a scheduled run from firing mid-teardown.
# 1. Stop the timers before anything else
sudo systemctl disable --now linumed-base-backup.timer linumed-base-restore-test.timer
# 2. Stop and remove the service stacks
for stack in orthanc bridgelink monitoring caddy; do
[ -d "/opt/linumed-base/$stack" ] && \
sudo docker compose -f "/opt/linumed-base/$stack/docker-compose.yml" down
done
# 3. Data. Irreversible - verify the backup first, see backup-restore.md
# Volumes are named after the Compose project, NOT linumed-base - see the table above.
sudo docker volume ls --format '{{.Name}}' \
| grep -E '^(caddy|monitoring|bridgelink|orthanc)_' # look before removing
# sudo docker volume rm <each one>
# 4. Files, including the secrets under /opt/linumed-base/*/secrets/
sudo rm -rf /opt/linumed-base
sudo rm -f /etc/ssh/sshd_config.d/10-linumed-hardening.conf \
/etc/ssh/sshd_config.d/90-linumed-tunnel-users.conf \
/etc/fail2ban/jail.d/10-linumed-sshd.conf \
/etc/apt/apt.conf.d/51-linumed-unattended-upgrades \
/etc/systemd/timesyncd.conf.d/10-linumed.conf \
/etc/default/prometheus-node-exporter \
/usr/local/bin/linumed-base-backup.sh \
/usr/local/bin/linumed-base-restore-test.sh \
/etc/systemd/system/linumed-base-*.service \
/etc/systemd/system/linumed-base-*.timer
sudo systemctl daemon-reload
Restart ssh last, and keep a second session open while you do. Removing the hardening
drop-in changes the port and the authentication policy back to the distribution defaults; if
the port changes and something goes wrong, an open session is the only way back in.
sudo systemctl restart ssh # keep another session open
What the commands above deliberately leave¶
Not oversights - each one would make the host worse or is not this kit's to decide:
/etc/apt/apt.conf.d/20auto-upgradeskeeps unattended security updates running. Removing it silently stops them, which is the last thing an abandoned host needs./etc/apt/sources.list.d/docker.listand/etc/apt/keyrings/docker.ascstay because Docker itself stays. Remove them only if Docker is going too./etc/docker/daemon.jsonholds log rotation. Removing it lets container logs grow without limit again./etc/systemd/system/ssh.socket.d/listen.confexists only if the SSH port was changed. Remove it only together with the hardening drop-in and in the same keep-a-session-open manner, or the port the sockets listen on and the port the config expects can end up disagreeing.- Packages and ufw rules, for the reasoning at the top. Whether
ufwshould keep denying by default after Linumed Base is gone is a decision about that host, not about this kit.
Verifying it is gone¶
systemctl list-units 'linumed-base-*' # expect nothing
sudo docker ps -a --filter name=linumed-base # expect nothing
ls /opt/linumed-base 2>/dev/null # expect "No such file"
sudo sshd -T | grep -E '^(port|passwordauthentication)'
The last one is the check worth doing: it shows the SSH policy actually in effect, which is what a stale drop-in would otherwise hide.
This procedure is executed, not just written¶
test/vm-test.sh runs the steps on this page against the throwaway VM as its final act,
after every other check, and asserts the verification commands above come back clean -
including reconnecting over SSH after the restart the warning is about. The VM is destroyed
immediately afterwards, so the run costs almost nothing.
The reason for bothering: a removal procedure that has never been executed is prose that looks verified because it is detailed. If this page and the roles drift apart - a new file in a new role, say, that nothing here removes - the test fails rather than the operator finding out on a production host.