landfall 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +482 -2
- package/dist/cli.mjs +684 -0
- package/dist/index.html +1511 -0
- package/dist/job-i5GF9h93.mjs +2 -0
- package/dist/server-CqBtraHg.mjs +4 -0
- package/dist/tui-BDvEukfL.mjs +8 -0
- package/package.json +45 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ph1p
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,483 @@
|
|
|
1
|
-
#
|
|
1
|
+
# landfall
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Set up Linux servers and deploy sites to them behind [Traefik](https://traefik.io) with automatic HTTPS, from one CLI.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i -g landfall
|
|
7
|
+
lf setup het --host 203.0.113.7 # fresh Ubuntu/Debian box → Docker, firewall, Traefik, ssh key
|
|
8
|
+
cd my-site && lf deploy -s het # live at https://my-site.203-0-113-7.sslip.io
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **Servers are ssh targets.** Any `~/.ssh/config` alias works. If `ssh het` works, `lf deploy -s het` works.
|
|
12
|
+
- **No agent on the server.** Everything is plain Docker Compose under `/opt/landfall`, so you can read and fix it by hand.
|
|
13
|
+
- **Every change is journaled** on the server and can be undone with `lf undo`.
|
|
14
|
+
- **Three ways in:** plain commands for scripts, CI and agents; a web dashboard (`lf`); a terminal dashboard (`lf tui`).
|
|
15
|
+
|
|
16
|
+
`lf` is short for `landfall`. Both commands are installed.
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [Concepts](#concepts)
|
|
22
|
+
- [Using landfall from scripts and agents](#using-landfall-from-scripts-and-agents)
|
|
23
|
+
- [Command reference](#command-reference)
|
|
24
|
+
- [Prepare a server](#prepare-a-server)
|
|
25
|
+
- [Deploy a site](#deploy-a-site)
|
|
26
|
+
- [Day-to-day](#day-to-day)
|
|
27
|
+
- [Updates](#updates)
|
|
28
|
+
- [Traefik](#traefik)
|
|
29
|
+
- [Firewall](#firewall)
|
|
30
|
+
- [History and undo](#history-and-undo)
|
|
31
|
+
- [Managing servers](#managing-servers)
|
|
32
|
+
- [Dashboards](#dashboards)
|
|
33
|
+
- [Files and config](#files-and-config)
|
|
34
|
+
- [Troubleshooting](#troubleshooting)
|
|
35
|
+
- [Development](#development)
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
You need Node.js 22+ plus `ssh`, `ssh-copy-id` and `tar` on your machine. The server must run Ubuntu or Debian
|
|
40
|
+
and accept a login as root or as a user with passwordless sudo. landfall installs Docker itself.
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm i -g landfall # or: pnpm add -g landfall
|
|
44
|
+
lf --help # all commands
|
|
45
|
+
lf <command> --help # every option of one command
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Concepts
|
|
49
|
+
|
|
50
|
+
| term | meaning |
|
|
51
|
+
|---|---|
|
|
52
|
+
| **server** | An ssh target: a `~/.ssh/config` alias, or one landfall registered with `lf server add`/`lf setup`. Pass it as `-s <name>`. The first server you add becomes the default. |
|
|
53
|
+
| **site** | One deployed thing on a server: a static folder, a Dockerfile app or a prebuilt image. It runs as container `landfall-<name>` and Traefik routes its domains to it. |
|
|
54
|
+
| **project file** | `./.landfall.json`, written by the first `lf deploy`: `{ "name", "server", "domains" }`. Commands that take `[name]` default to this site, and `-s` defaults to its server. |
|
|
55
|
+
| **main domain** | Optional per-server domain (`lf server domain het example.com`). Sites then default to `<site>.example.com`, and a bare name like `shop` means `shop.example.com`. Without one, sites get `<site>.<ip-with-dashes>.sslip.io` (e.g. `blog.203-0-113-7.sslip.io`), which resolves without any DNS setup. |
|
|
56
|
+
| **release** | Every deploy creates one, with an id like `20260122143005`. The last 5 are kept. `lf rollback` switches between them. |
|
|
57
|
+
| **run** | One landfall invocation that changed something on a server. Runs are listed by `lf history` and reverted by `lf undo`. |
|
|
58
|
+
|
|
59
|
+
How defaults resolve, for every command:
|
|
60
|
+
|
|
61
|
+
- **site** `[name]`: the argument, else `name` from `./.landfall.json`.
|
|
62
|
+
- **server** `-s`: the flag, else `server` from `./.landfall.json`, else the default server (`lf server use`).
|
|
63
|
+
|
|
64
|
+
## Using landfall from scripts and agents
|
|
65
|
+
|
|
66
|
+
Every dashboard action is a plain command, and both dashboards print the command they run (`$ lf logs blog -s het -f`).
|
|
67
|
+
|
|
68
|
+
- **No prompts:** `-y` accepts confirmations and takes defaults. Without a terminal, prompts are never shown. A
|
|
69
|
+
command that would ask then **fails** and tells you to pass `-y`. It never silently goes ahead.
|
|
70
|
+
- **Passwords:** pass them with `--password-stdin` or `$LANDFALL_PASSWORD` (`setup`, `server add`). For `lf auth`, use `-p`.
|
|
71
|
+
- **JSON output:** `--json` works on `ls`, `info`, `server config`, `server domain`, `domains`, `firewall`,
|
|
72
|
+
`updates` and `history`.
|
|
73
|
+
- **Dry runs:** `lf setup <name> --check` changes nothing. `lf updates` only reports.
|
|
74
|
+
- **Exit codes:** `0` means success, `1` means error (the message starts with `error:` on stderr), and `2` means unknown command.
|
|
75
|
+
- **Safety rails:** `-y` never overrides a safety refusal, for example a firewall change that would lock out ssh, or
|
|
76
|
+
setup steps that would break running services. Only `--force` does.
|
|
77
|
+
|
|
78
|
+
A typical unattended flow:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
printf '%s' "$ROOT_PW" | lf setup box --host 203.0.113.7 --password-stdin -y
|
|
82
|
+
lf server domain box example.com -y
|
|
83
|
+
lf deploy ./dist -n blog -s box -y # → https://blog.example.com
|
|
84
|
+
lf ls -s box --json
|
|
85
|
+
lf history -s box --json
|
|
86
|
+
lf undo -s box -y # revert the last run
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Command reference
|
|
90
|
+
|
|
91
|
+
`[name]` is a site or server and is optional (see [Concepts](#concepts)). Most site and server commands take `-s <server>`. Every command takes `-y` and `--help`.
|
|
92
|
+
|
|
93
|
+
**Servers**
|
|
94
|
+
|
|
95
|
+
| command | what it does |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `lf setup <name> [--host ip]` | Turns a box into a deploy target: ssh key, Docker, ufw, fail2ban, deploy user, ssh hardening and Traefik. Idempotent. |
|
|
98
|
+
| `lf setup <name> --check` | Reports what is set up and what setup would change. Changes nothing. |
|
|
99
|
+
| `lf setup <name> --key-only` | Only installs landfall's ssh key. |
|
|
100
|
+
| `lf server add <name> [--host ip] [-u user] [-p port] [-i key] [--password-stdin] [--domain d] [--default]` | Registers a server without installing anything on it. Writes `Host <name>` to `~/.ssh/config`. |
|
|
101
|
+
| `lf server list [--all]` | Lists registered servers. `--all` adds every ssh alias. Alias: `servers`. |
|
|
102
|
+
| `lf server info [name] [--json]` | Shows OS and release support, kernel, uptime, CPU, RAM, disk, addresses and containers. Alias: `info`. |
|
|
103
|
+
| `lf server config [name] [--json]` | Shows everything landfall and ssh use for a server. Does not connect. |
|
|
104
|
+
| `lf server domain [name] [domain...] [--add d] [--primary d] [--rm d] [--clear] [--json]` | Manages main domains (the first is primary) and checks their DNS. |
|
|
105
|
+
| `lf server use <name>` | Sets the default server. |
|
|
106
|
+
| `lf server rm <name>` | Forgets a server locally. The server is untouched. |
|
|
107
|
+
| `lf ssh [name] [-- cmd]` | Opens a shell or runs a one-off command. |
|
|
108
|
+
| `lf keygen <name>` | Creates `~/.ssh/landfall_<name>` and prints the public key. |
|
|
109
|
+
|
|
110
|
+
**Sites**
|
|
111
|
+
|
|
112
|
+
| command | what it does |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `lf deploy [dir] [-d domain...] [-n name] [-b cmd] [--spa] [--image img] [--port n] [-e K=V] [--env-file f] [--static] [--keep n]` | Deploys a static folder, a Dockerfile app or an image. |
|
|
115
|
+
| `lf ls [--json]` | Lists sites on the server with their status. Alias: `sites list`. |
|
|
116
|
+
| `lf sites info [name]` | Shows domains, container and release history. |
|
|
117
|
+
| `lf domains [name] [--add d] [--rm d] [--set d] [--json]` | Lists domains with their DNS and HTTPS state, or changes them. |
|
|
118
|
+
| `lf auth [name] -u user [-p pw]` / `--rm-user u` / `--off` | Puts basic-auth login in front of a site. |
|
|
119
|
+
| `lf open [name]` | Opens the site in the browser. |
|
|
120
|
+
| `lf logs [name] [-f] [-n lines]` | Shows container logs. |
|
|
121
|
+
| `lf env [name] [K=V...] [-u K]` | Shows or changes env vars. Recreates the container. |
|
|
122
|
+
| `lf rollback [name] [--to id]` | Switches to the previous release, or to the given one. |
|
|
123
|
+
| `lf restart` / `stop` / `start [name]` | Restarts, stops or starts the site. |
|
|
124
|
+
| `lf sites rm <name>` | Deletes the site and its files. |
|
|
125
|
+
|
|
126
|
+
**Traefik, firewall, updates, history**
|
|
127
|
+
|
|
128
|
+
| command | what it does |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `lf traefik setup [--email e] [--dashboard host\|traefik\|off] [--staging] [--image img] [--host-network]` | Installs or reconfigures landfall's Traefik. |
|
|
131
|
+
| `lf traefik use [container] [--network n --entrypoint e --resolver r]` | Routes sites through a Traefik you already run. |
|
|
132
|
+
| `lf traefik status` / `logs [-f]` / `restart` | Inspects or restarts Traefik. `lf traefik` alone means `status`. |
|
|
133
|
+
| `lf firewall [--json]` | Lists ufw rules and, for each listening port, whether it is reachable. |
|
|
134
|
+
| `lf firewall allow` / `deny <port[/proto]> [--from ip] [--comment c]` | Adds a rule. A deny goes first, so it wins. |
|
|
135
|
+
| `lf firewall update <n> [--port p] [--from ip] [--action allow\|deny\|limit]` | Changes a rule. |
|
|
136
|
+
| `lf firewall rm <n>` / `enable` / `disable` | Removes a rule, or turns ufw on or off. |
|
|
137
|
+
| `lf updates [--no-refresh] [--all] [--json]` | Shows pending OS packages, newer images, and Docker/compose versions. Changes nothing. |
|
|
138
|
+
| `lf update [site...] [--packages] [--security] [--reboot] [--images] [--compose]` | Applies updates. Image and compose updates can be undone. |
|
|
139
|
+
| `lf history [-n 20] [--json]` | Lists what landfall changed on the server, run by run. Alias: `log`. |
|
|
140
|
+
| `lf undo [n \| n.m] [--all] [--force]` | Reverts the latest run, run `n`, step `n.m`, or everything. Alias: `revert`. |
|
|
141
|
+
|
|
142
|
+
**Dashboards**
|
|
143
|
+
|
|
144
|
+
| command | what it does |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `lf` / `lf ui [--port 4777] [--no-open]` | Starts the web dashboard on 127.0.0.1 and prints a login link. |
|
|
147
|
+
| `lf tui` | Starts the terminal dashboard. |
|
|
148
|
+
|
|
149
|
+
## Prepare a server
|
|
150
|
+
|
|
151
|
+
### A fresh server (only a root password)
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
lf setup het --host 203.0.113.7
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
1. Creates `~/.ssh/landfall_het` and installs it with `ssh-copy-id`. You type the root password once.
|
|
158
|
+
2. Installs Docker, ufw (allowing ssh, 80 and 443), fail2ban and unattended-upgrades.
|
|
159
|
+
3. Creates the user `deploy` in the docker group and disables ssh password login. Root keeps key-only access.
|
|
160
|
+
4. Writes `Host het` to `~/.ssh/config`, creating the file if needed. `ssh het` now logs in as `deploy`.
|
|
161
|
+
5. Starts Traefik, which gets Let's Encrypt certificates for every site.
|
|
162
|
+
|
|
163
|
+
Setup shows what it is about to change and asks once (`-y` skips the question). Key login is checked **before**
|
|
164
|
+
password login is turned off, so a failed key setup can't lock you out.
|
|
165
|
+
|
|
166
|
+
### A server you already reach with `ssh het`
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
lf setup het --check # what is there, what would change
|
|
170
|
+
lf setup het
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
landfall reads host, user, port and key from your ssh config (`ssh -G`) and keeps a key that works. It inspects
|
|
174
|
+
first and prints what is done (`✓`), what it will do (`→`) and what it refuses to touch (`!`):
|
|
175
|
+
|
|
176
|
+
| step | skipped when |
|
|
177
|
+
|---|---|
|
|
178
|
+
| firewall | ufw is off and enabling it would block a public port that something listens on (Postgres on 5432, WireGuard on 51820/udp, …). Docker-published ports stay reachable because Docker's NAT bypasses ufw. If Docker uses nftables or has no IPv6 NAT, they would be blocked, so setup skips the firewall. |
|
|
179
|
+
| ssh hardening | other users still log in with a password |
|
|
180
|
+
| traefik | 80/443 are held by something that is not Traefik, or by a `traefik` that landfall didn't create. If it is your own Traefik, setup offers `lf traefik use` instead. |
|
|
181
|
+
|
|
182
|
+
Docker that is installed but stopped gets started. A missing compose, or the old v1 `docker-compose`, is replaced
|
|
183
|
+
with the newest release. An existing Docker is never reinstalled. In a terminal, setup asks about each conflict
|
|
184
|
+
(the default is no). `-y` never accepts a conflict. Only `--force` does. Existing sites are never touched.
|
|
185
|
+
|
|
186
|
+
### Setup flags
|
|
187
|
+
|
|
188
|
+
| flag | effect |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `-u admin` / `-p 2222` | log in as `admin` (needs passwordless sudo) / ssh port |
|
|
191
|
+
| `-i ~/.ssh/id_ed25519` | use an existing key instead of generating one |
|
|
192
|
+
| `--password-stdin` | read the login password from stdin (or set `$LANDFALL_PASSWORD`) |
|
|
193
|
+
| `--deploy-user ""` | deploy as the login user instead of creating `deploy` |
|
|
194
|
+
| `--domain example.com` | primary main domain |
|
|
195
|
+
| `--staging` / `--no-staging` | use Let's Encrypt's staging CA while testing (remembered) |
|
|
196
|
+
| `--dashboard traefik.example.com` | Traefik dashboard behind basic auth (`traefik` alone means `traefik.<main domain>`) |
|
|
197
|
+
| `--image traefik:v3.5` | Traefik image (remembered, default `traefik:v3`) |
|
|
198
|
+
| `--host-network` | run Traefik on the host network (remembered), see [Traefik](#traefik) |
|
|
199
|
+
| `--no-firewall` / `--no-harden` / `--no-traefik` / `--no-ssh-config` | skip a step |
|
|
200
|
+
| `--check` / `--key-only` / `--force` | report only / install only the key / apply despite conflicts |
|
|
201
|
+
|
|
202
|
+
On a first Traefik install, setup asks for the image, host network, e-mail and dashboard in a terminal. `-y`
|
|
203
|
+
takes the defaults. The e-mail is optional, because Let's Encrypt no longer sends expiry mails.
|
|
204
|
+
|
|
205
|
+
## Deploy a site
|
|
206
|
+
|
|
207
|
+
Point the domain's A/AAAA record at the server first. Let's Encrypt needs to reach ports 80 and 443.
|
|
208
|
+
No domain yet? Leave out `-d`. landfall offers `<name>.<ip-with-dashes>.sslip.io` (`-y` accepts it) and the site has HTTPS right away.
|
|
209
|
+
|
|
210
|
+
| kind | when | how |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| **static** | default | Uploads the folder and serves it with `nginx:alpine` (gzip, long cache headers for assets). The folder is `.` if it has an `index.html`, else the first of `dist/ build/ out/ public/ _site/ site/`. `--spa` sends unknown paths to `index.html`. |
|
|
213
|
+
| **dockerfile** | the folder has a `Dockerfile` | Uploads the folder and builds it on the server. The port comes from `EXPOSE`, else `--port` (default 3000). `--static` forces static. |
|
|
214
|
+
| **image** | `--image ghcr.io/me/app:1.2` | Pulls the image and runs it. Needs `--port`. For private registries: `lf ssh het -- docker login ghcr.io`. |
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
cd my-site
|
|
218
|
+
lf deploy -d example.com -d www.example.com -s het # static, name = folder name (-n to override)
|
|
219
|
+
lf deploy -b "npm run build" # build locally first
|
|
220
|
+
lf deploy -d app.example.com -e DATABASE_URL=postgres://… --env-file .env.production # Dockerfile app
|
|
221
|
+
lf deploy -n api --image ghcr.io/me/api:1.4 --port 8080 -d api.example.com # image
|
|
222
|
+
lf deploy # later: redeploy using ./.landfall.json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The first deploy writes `.landfall.json`. Commit it:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{ "name": "my-site", "server": "het", "domains": ["example.com", "www.example.com"] }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Local `.env*` files are never uploaded. Pass variables with `-e`/`--env-file`, or later with `lf env`.
|
|
232
|
+
Switching a static release only reloads nginx, so there is no downtime. `--keep 10` keeps more releases.
|
|
233
|
+
|
|
234
|
+
### Main domains
|
|
235
|
+
|
|
236
|
+
```sh
|
|
237
|
+
lf server domain het example.com # checks DNS for example.com and *.example.com
|
|
238
|
+
lf deploy # site "blog" → blog.example.com
|
|
239
|
+
lf deploy -d shop # no dot → shop.example.com
|
|
240
|
+
lf domains blog --add www # www.example.com
|
|
241
|
+
lf deploy -d @ # @ → example.com itself
|
|
242
|
+
lf domains blog --add @ # blog also serves example.com
|
|
243
|
+
lf server domain het --add other.org # more main domains; the first stays primary
|
|
244
|
+
lf server domain het --primary other.org # switch the primary
|
|
245
|
+
lf server domain het --rm other.org # drop one; --clear drops all (back to sslip.io)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`server domain` prints the A records to add: one for the domain and a wildcard `*.example.com`. With those in
|
|
249
|
+
place, every new site resolves without touching DNS again. For non-primary main domains, write the full name (`-d shop.other.org`).
|
|
250
|
+
|
|
251
|
+
A domain belongs to one site. Giving it to another one (`lf deploy -d @`, `lf domains shop --add example.com`)
|
|
252
|
+
asks, then moves it: the old site keeps its other domains. A move that would leave a site with none is refused,
|
|
253
|
+
and so is a redeploy whose domain comes only from `.landfall.json` (pass `-d` to move it on purpose).
|
|
254
|
+
|
|
255
|
+
## Day-to-day
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
lf ls # sites with status
|
|
259
|
+
lf sites info # domains, container, releases
|
|
260
|
+
lf logs -f # follow logs (-n 500 for more lines)
|
|
261
|
+
lf rollback # previous release (--to <id> for a specific one)
|
|
262
|
+
lf env API_KEY=abc # set env (container is recreated); -u KEY removes one
|
|
263
|
+
lf restart | stop | start
|
|
264
|
+
lf domains # where DNS points, whether HTTPS answers
|
|
265
|
+
lf domains --add www.example.com # also --rm, --set (updates .landfall.json)
|
|
266
|
+
lf auth -u anna # login in front of the site (asks for the password, -p passes it)
|
|
267
|
+
lf auth --rm-user anna | --off
|
|
268
|
+
lf open
|
|
269
|
+
lf sites rm blog # delete the site (asks you to type the name)
|
|
270
|
+
lf logs api -s het # another site on another server
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Commands that remove something ask first: `sites rm`, `domains --rm/--set`, `server domain --rm/--clear`,
|
|
274
|
+
`auth --rm-user/--off`, `firewall rm`, `undo` and `server rm`. `sites rm` and `undo --all` want the name typed
|
|
275
|
+
back. Domain and login changes are journaled, so `lf undo` reverts them.
|
|
276
|
+
|
|
277
|
+
## Updates
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
lf updates # packages (security marked, reboot needed), images, Docker/compose versions
|
|
281
|
+
lf update # upgrade packages and pull newer images (asks first)
|
|
282
|
+
lf update --packages --security # only security updates (apt, dnf)
|
|
283
|
+
lf update --packages --reboot # reboot afterwards if needed
|
|
284
|
+
lf update --images # Traefik and image sites: pull, recreate
|
|
285
|
+
lf update blog --images # one site
|
|
286
|
+
lf update --compose # newest compose, where landfall installed it
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Package upgrades (apt-get, dnf/yum, apk) cannot be undone. Image and compose updates record the previous version,
|
|
290
|
+
so `lf undo` restores it. Landfall never touches images pinned by digest, Dockerfile sites or foreign containers.
|
|
291
|
+
Version-pinned tags show the newest tag of the same shape (`traefik:v3.6` → `traefik:v3.7`).
|
|
292
|
+
|
|
293
|
+
## Traefik
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
lf traefik status # container state, routes, mode
|
|
297
|
+
lf traefik logs -f # certificate errors show up here
|
|
298
|
+
lf traefik setup --email you@example.com # reconfigure (safe to re-run)
|
|
299
|
+
lf traefik setup --dashboard traefik # dashboard at traefik.<main domain>; `off` removes it
|
|
300
|
+
lf traefik setup --host-network # bind 80/443 on the host
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
landfall's Traefik is the container `traefik` from `/opt/landfall/traefik/docker-compose.yml`. By default Docker
|
|
304
|
+
publishes 80/443 to it. Sites work the same in both modes:
|
|
305
|
+
|
|
306
|
+
| | ports published (default) | `--host-network` |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| client IP in logs / `X-Forwarded-For` | real IPv4; IPv6 may show Docker's gateway | always real |
|
|
309
|
+
| ufw | Docker's NAT bypasses ufw for 80/443 | ufw applies |
|
|
310
|
+
|
|
311
|
+
Traefik only answers hosts it has a certificate for (`sniStrict`). A domain pointed at the server but served
|
|
312
|
+
by no site fails the TLS handshake, as if nothing listened there, instead of showing a 404 or Traefik's own
|
|
313
|
+
certificate; plain HTTP to it redirects to HTTPS and fails the same way. A new site's domain fails the same way
|
|
314
|
+
for the seconds until Let's Encrypt issues its certificate. Your own certificates and Traefik config go into
|
|
315
|
+
`/opt/landfall/traefik/dynamic/`: landfall rewrites only `landfall.yml` there.
|
|
316
|
+
|
|
317
|
+
The dashboard password is generated and printed once (`--dashboard-password` sets your own). Image, network mode,
|
|
318
|
+
e-mail, staging and dashboard are remembered per server.
|
|
319
|
+
|
|
320
|
+
**Using a Traefik you already run.** If 80/443 belong to your own Traefik container, landfall does not install a second one:
|
|
321
|
+
|
|
322
|
+
```sh
|
|
323
|
+
lf traefik use -s het # finds the container on :80/:443
|
|
324
|
+
lf traefik use traefik-reverseproxy-1 -s het # or name it
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
landfall reads the network, HTTPS entrypoint and cert resolver from the container's flags. If your Traefik is
|
|
328
|
+
configured by a file, pass `--network proxy --entrypoint websecure --resolver letsencrypt`. The choice is stored
|
|
329
|
+
in `/opt/landfall/proxy.env`. landfall never changes that Traefik.
|
|
330
|
+
|
|
331
|
+
## Firewall
|
|
332
|
+
|
|
333
|
+
```sh
|
|
334
|
+
lf firewall -s het # rules + each listening port: open, limited, blocked, localhost only
|
|
335
|
+
lf firewall allow 51820/udp
|
|
336
|
+
lf firewall allow 5432 --from 10.0.0.0/8 --comment "db, lan only"
|
|
337
|
+
lf firewall deny 8080 # goes before the allow rules, so it wins
|
|
338
|
+
lf firewall update 3 --action deny # numbers as `lf firewall` prints them
|
|
339
|
+
lf firewall rm 3 # or by text: lf firewall rm "allow 8080"
|
|
340
|
+
lf firewall enable | disable # enable installs ufw if needed and allows ssh first
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
- Changes run as root or with sudo. If the user has neither (like `deploy`), landfall uses root's key login.
|
|
344
|
+
- A change that would block the ssh port, or `enable` while ports in use have no rule, is refused. `--force` overrides.
|
|
345
|
+
- Docker-published ports bypass ufw, so a deny rule doesn't close them. Bind them to `127.0.0.1` in their compose file instead.
|
|
346
|
+
- `update` replaces the rule. If ufw rejects the new rule, the old one is put back.
|
|
347
|
+
|
|
348
|
+
## History and undo
|
|
349
|
+
|
|
350
|
+
Every change landfall makes is appended to `/opt/landfall/journal.log` on the server: when, which local user and
|
|
351
|
+
command, what changed and the shell that reverts it.
|
|
352
|
+
|
|
353
|
+
```sh
|
|
354
|
+
lf history -s het # newest first; ↺ undoable, · not undoable, ✓ already undone
|
|
355
|
+
lf undo -s het # revert the latest run with something left to undo
|
|
356
|
+
lf undo 4 # run #4
|
|
357
|
+
lf undo 4.2 # only its second step
|
|
358
|
+
lf undo --all # everything, newest first
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- **Undoable:** setup (packages and Docker stay installed), firewall rules and on/off, deploys and rollbacks
|
|
362
|
+
(previous release and env), env changes, stop/start, domains, logins, Traefik setup and `traefik use`, image and
|
|
363
|
+
compose updates.
|
|
364
|
+
- **Not undoable:** `sites rm`, restarts, package upgrades, and a firewall rule that replaced an existing one.
|
|
365
|
+
- Steps revert newest first, each as the user that made it. The first failure stops the rest.
|
|
366
|
+
- Undo changes the server only. Local `~/.config/landfall` and `.landfall.json` stay as they are.
|
|
367
|
+
- Overwritten files are backed up as `.<name>.landfall-<run>-<n>`, and the newest 10 are kept per file.
|
|
368
|
+
|
|
369
|
+
## Managing servers
|
|
370
|
+
|
|
371
|
+
```sh
|
|
372
|
+
lf server add het # register an existing ssh alias
|
|
373
|
+
lf server add box --host 1.2.3.4 -u ubuntu -i ~/.ssh/box --default
|
|
374
|
+
lf server add web --host 1.2.3.4 -u admin # asks the password once, installs ~/.ssh/landfall_web
|
|
375
|
+
lf server config het # landfall entry, Traefik settings, ssh login, keys, Host block
|
|
376
|
+
lf server list --all
|
|
377
|
+
lf server use het
|
|
378
|
+
lf server rm het # forget locally
|
|
379
|
+
lf ssh het -- docker ps
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`server add` installs nothing on the server. It writes a `Host <name>` block between `# >>> landfall:<name>` markers
|
|
383
|
+
(`--no-ssh-config` keeps the details in landfall's config instead). In a Host block you wrote yourself, landfall
|
|
384
|
+
only steps in when the key does not work. It then puts the working `IdentityFile` first, drops lines whose file is
|
|
385
|
+
gone, and keeps a backup as `~/.ssh/config.landfall-bak`.
|
|
386
|
+
|
|
387
|
+
## Dashboards
|
|
388
|
+
|
|
389
|
+
**Web (`lf` or `lf ui`).** The dashboard listens on `127.0.0.1` and opens your browser. Servers are on the left
|
|
390
|
+
(★ marks the default). A server tab has Sites, Updates, Firewall, History and Settings. **Deploy folder…** browses
|
|
391
|
+
local folders or takes a dropped folder or `.zip`. Every action streams the output of its plain command into a side
|
|
392
|
+
panel. Destructive actions ask first. Login works through the printed `/login?t=…` link. The token is kept in
|
|
393
|
+
`~/.config/landfall/ui-token`, and you can delete that file to rotate it. Requests with a foreign `Host` or `Origin`
|
|
394
|
+
are refused. Running `lf` again while the dashboard is running just opens the page.
|
|
395
|
+
|
|
396
|
+
**Terminal (`lf tui`).**
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
landfall 0.1.0 this folder: blog → het · blog.example.com
|
|
400
|
+
╭ Servers ──────────╮╭ Sites on het traefik Up 3 days ─────────────────────╮
|
|
401
|
+
│ › ● het default │ │ › ● blog static blog.example.com 2h ago │
|
|
402
|
+
│ ● box │ │ ● api dockerfile api.example.com 5d ago │
|
|
403
|
+
│ ○ nas ssh config│ │ │
|
|
404
|
+
│ + add a server │ │ │
|
|
405
|
+
╰───────────────────╯╰──────────────────────────────────────────────────────╯
|
|
406
|
+
? help q quit ↑↓ server ↵ sites u check & set up d deploy here …
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Keys: `↵` connect/open, `d` deploy this folder, `u` check and set up, `a` add a server, `f` firewall, `h` history,
|
|
410
|
+
`z` undo, `x` shell, `esc` stop/back, `?` all keys. Dot colors: green ready, yellow not set up, red unreachable, ○ not connected.
|
|
411
|
+
|
|
412
|
+
## Files and config
|
|
413
|
+
|
|
414
|
+
| local file | content |
|
|
415
|
+
|---|---|
|
|
416
|
+
| `~/.config/landfall/config.json` | registered servers, default server, per-server settings |
|
|
417
|
+
| `~/.config/landfall/projects.json` | folders deployed from this machine (dashboard *Recent*/*Redeploy*) |
|
|
418
|
+
| `~/.config/landfall/ui-token` | web dashboard login token |
|
|
419
|
+
| `~/.config/landfall/uploads/` | folders dropped into the dashboard, removed after a day |
|
|
420
|
+
| `~/.ssh/landfall_<name>` | generated keys |
|
|
421
|
+
| `~/.ssh/config` | `Host <name>` blocks between `# >>> landfall:<name>` markers |
|
|
422
|
+
| `./.landfall.json` | per-project deploy settings |
|
|
423
|
+
|
|
424
|
+
| env var | effect |
|
|
425
|
+
|---|---|
|
|
426
|
+
| `LANDFALL_CONFIG_DIR` | use another directory than `~/.config/landfall` |
|
|
427
|
+
| `LANDFALL_SSH_CONFIG` | use another ssh config file (`ssh -F`) |
|
|
428
|
+
| `LANDFALL_PASSWORD` | login password for `setup`/`server add` |
|
|
429
|
+
|
|
430
|
+
**Coming from `srv`** (landfall's old name): `~/.config/srv` moves to `~/.config/landfall` on the first run, and
|
|
431
|
+
`srv.json`, `~/.ssh/srv_<name>`, `# >>> srv:` blocks and `$SRV_*` variables keep working. A server set up by `srv`
|
|
432
|
+
still has `/opt/srv`: other commands refuse it until `lf setup <name>` moves it to `/opt/landfall` and renames
|
|
433
|
+
the site containers. Sites keep serving, Traefik restarts once, and older changes can no longer be undone.
|
|
434
|
+
All of this lives in `src/legacy.ts`.
|
|
435
|
+
|
|
436
|
+
On the server:
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
/opt/landfall/
|
|
440
|
+
traefik/docker-compose.yml Traefik, ports 80/443, Let's Encrypt (HTTP-01)
|
|
441
|
+
traefik/letsencrypt/acme.json
|
|
442
|
+
proxy.env only with `lf traefik use`
|
|
443
|
+
sites/<name>/
|
|
444
|
+
site.json metadata + release history
|
|
445
|
+
docker-compose.yml container landfall-<name> on network "web" with Traefik labels
|
|
446
|
+
nginx.conf static sites
|
|
447
|
+
.env app sites (0600)
|
|
448
|
+
releases/<id>/ last 5 kept
|
|
449
|
+
current -> releases/<id>
|
|
450
|
+
journal.log every change, with the shell that reverts it
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`cd /opt/landfall/sites/blog && docker compose ps` works as usual.
|
|
454
|
+
|
|
455
|
+
## Troubleshooting
|
|
456
|
+
|
|
457
|
+
| symptom | fix |
|
|
458
|
+
|---|---|
|
|
459
|
+
| `setup needs root or passwordless sudo` | use `-u root`, or give your user passwordless sudo |
|
|
460
|
+
| certificate warning in the browser | you used `--staging`: `lf setup <name> --no-staging` |
|
|
461
|
+
| no certificate at all | DNS must point at the server and 80/443 must be open; see `lf traefik logs` |
|
|
462
|
+
| TLS handshake fails | no site serves the domain (`lf domains --add …`), or its certificate isn't issued yet: `lf traefik logs` |
|
|
463
|
+
| site container keeps restarting | `lf logs`; for apps check `--port` and env |
|
|
464
|
+
| `skipping firewall/ssh hardening: …` | setup protected something running there. Fix it, or re-run with `--force` |
|
|
465
|
+
| `skipping traefik: port 80 is in use by …` | free the port, then `lf traefik setup`. If it's a Traefik, `lf traefik use` |
|
|
466
|
+
| `… blocks ssh port 22 … (--force to do it anyway)` | `lf firewall allow 22/tcp` first, or force if you have another way in |
|
|
467
|
+
| `ssh cannot resolve <name>` | not in your ssh config: `lf server add <name> --host …` |
|
|
468
|
+
| `… Pass -y to confirm (no terminal to ask)` | non-interactive run of a command that asks: add `-y` |
|
|
469
|
+
|
|
470
|
+
## Development
|
|
471
|
+
|
|
472
|
+
```sh
|
|
473
|
+
pnpm install
|
|
474
|
+
pnpm dev <command> # run from source (tsx)
|
|
475
|
+
pnpm test # unit tests (vitest)
|
|
476
|
+
pnpm typecheck
|
|
477
|
+
pnpm build # tsdown → dist/cli.mjs, copies the web page next to it
|
|
478
|
+
pnpm e2e # full run against a throwaway sshd+dockerd container (needs Docker)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## License
|
|
482
|
+
|
|
483
|
+
MIT
|