# Server Manager — full help content

This file concatenates every article from the Server Manager help center, in markdown. Each article is preceded by its canonical URL and category for traceability.

Source: https://servermanager.dev/llms.txt

---

# What is Server Manager?

URL: https://servermanager.dev/help/what-is-server-manager
Category: Get started
Last updated: 2026-05-24

> A web app that runs your server through plain English. Connect a Linux server, then deploy websites, install WordPress, manage databases, and configure email — all via a chat interface.

Server Manager is a web app that takes the hard parts out of running a Linux server (a [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), a dedicated box, a bare-metal machine, or even a home server). You connect it once (SSH credentials), and from then on you can:

- Deploy static websites and web apps in a few clicks
- Install WordPress, databases, and other ready-to-go services
- Manage backups, domains, email forwarding, and TLS certificates
- Get logs, health checks, and quick fixes when something breaks

All of it happens through a chat interface backed by an AI agent called **Faro** — you ask in plain English, Faro proposes the commands, you approve, Faro runs them. You see every command before it executes; nothing happens behind your back.

Server Manager is the **product** ([servermanager.dev](https://servermanager.dev)). Faro is the **agent** that does the work inside it.

---

# Deploy a website from your computer

URL: https://servermanager.dev/help/deploy-website-from-your-computer
Category: Deploy & manage sites and apps
Last updated: 2026-05-23

> Drag the folder of your built site into Server Manager, type your domain, click Deploy.

> Have your code on a git repo? → [Deploy a website from a git repo](/help/deploy-website-from-a-git-repo)

> Have a web app (Node, Python, Go) instead? → [Deploy a web app from your computer](/help/deploy-web-app-from-your-computer)

## 1. Open the Actions menu

In the top bar, click **Actions**. In the palette that opens, choose **Deploy from my computer** (under "Bring something in") — or just type "deploy" in the search box.

![Click Actions in the top bar, then choose Deploy from my computer](/help/deploy-website-from-your-computer/01-set-up-menu.svg)

## 2. Pick "Static site"

A new window opens. **Static site** is selected by default — leave it.

![The Deploy window opens with three tabs — Static site is selected by default](/help/deploy-website-from-your-computer/02-static-tab.svg)

## 3. Drop your folder

Drag the folder that contains your website into the dashed box. Or click **Pick a folder** and choose it.

![Drag your website folder into the dashed drop zone](/help/deploy-website-from-your-computer/03-drop-folder.svg)

## 4. Type your domain

Type the address you want the site to live at — for example, `mysite.example.com`. You can leave it blank for now and add a domain later.

![Type your domain in the Domain field](/help/deploy-website-from-your-computer/04-domain.svg)

## 5. Click Deploy

The button at the bottom shows how many files will be uploaded. Click it.

![Click the Deploy button at the bottom-right](/help/deploy-website-from-your-computer/05-deploy.svg)

## 6. Done

The window closes and the chat takes over — you'll see each step as it happens. When it finishes, your site appears on the home screen with a green dot.

![Your new site appears in the overview with a green dot](/help/deploy-website-from-your-computer/06-done.svg)

---

# Why can't I reach my port?

URL: https://servermanager.dev/help/why-cant-i-reach-my-port
Category: Networking & firewalls
Last updated: 2026-06-01

> "I can't reach my service from outside" is the same symptom for three different problems — each fixed in a different place. This article explains the three layers in plain English and shows you how Server Manager finds + fixes each one.

You set up a website, an app, a database, and from your laptop you can't reach it. The browser hangs. `curl` times out. You stare at the server and everything **inside** looks fine — the process is running, the logs say "listening on port 8080" — but from outside the world it's like the server isn't there.

This is one of the most common things people get stuck on. And the reason it's frustrating is that **three completely different problems all look identical from outside**. Until you know which one you're hitting, you can't fix it.

This article walks through the three layers, in the order you should check them, and shows how Server Manager finds + fixes each one.

## The three layers, in order

Between your browser and the program running on your server, there are three places where traffic can be blocked. From the outside, all three look the same — "I can't reach it." Inside, they're very different problems with very different fixes.

The three layers:

1. **Cloud-provider firewall** — runs **outside** your server, at your provider (Hetzner, Oracle, AWS, etc.). Drops packets before they reach the server at all.
2. **Server firewall** — runs **on** your server. Drops packets when they arrive, before any service sees them.
3. **Your service** — the program itself (Caddy, your app, your database). Either not running, or only listening locally.

Think of it like a building with three security checkpoints. The packet has to pass all three to reach you. If **any** one of them blocks it, you can't be reached — and from outside, you can't tell which checkpoint stopped it.

![Diagram: a packet flowing from "your laptop" through Layer 1 (cloud-provider firewall), Layer 2 (server firewall), Layer 3 (your service), reaching the service on the right. Below: three failure modes, one per layer.](/help/why-cant-i-reach-my-port/01-three-layers.svg)

## Layer 3 — Is your service actually listening?

The cheapest and most common cause. Before worrying about firewalls, check that there's a program on the server actively listening on the port you expect.

The two ways this goes wrong:

- **Nothing listening.** The container crashed. The systemd service failed to start. The app exited at boot. From outside this looks identical to a firewall block — the packet arrives, but nobody's home to answer.
- **Listening on localhost only.** This one is sneaky. Many programs default to "loopback only" (`127.0.0.1`), which means the program is happily running and accepting connections — **but only from the server itself**. Outside connections can't reach it even if every firewall is wide open. Common culprits: PostgreSQL out of the box, MySQL/MariaDB, Redis, Jupyter notebooks, dev servers, anything that says "for security, we only bind to localhost by default."

**How Server Manager helps:**

- The **Server details → Firewall tab** can answer "is my server-firewall blocking it?" but Layer 3 is something the chat is better at — ask Faro: *"Is anything actually listening on port 8080?"* and it'll run `ss -tlnp` and tell you.
- If you click **Diagnose unreachable** in the Firewall tab, the diagnostic checks all three layers — including this one — and tells you which is the cause.

## Layer 2 — The server firewall (on the server)

The next layer to check is the firewall **running on the server itself**. On Ubuntu/Debian it's usually `ufw`; on Fedora/Rocky it's `firewalld`; behind both is raw `iptables`. Whatever the front-end, the job is the same: drop incoming packets that aren't explicitly allowed.

Three things to know about this layer:

- **Most fresh servers are "default-allow"** — every port is reachable through the server firewall, because the firewall either isn't installed or isn't enforcing anything. Server Manager shows this clearly: the Firewall tab displays a yellow "Default-allow" banner when this is the case.
- **A "hardened" server is "default-deny"** — incoming traffic is blocked by default; only explicitly allowed ports get through. SSH (port 22) is always kept open, plus whatever you confirmed during setup.
- **Most hosting providers don't enable a server firewall for you.** You either get a default-allow server (every port open at this layer) or you set one up yourself.

**How Server Manager helps:**

- **Server details → Firewall tab** shows you the current state at a glance: backend (`ufw` / `iptables` / `firewalld` / `none`), active or default-allow, and the list of explicitly allowed ports with plain-English labels.
- **Open a port** lets you allow a single port through the server firewall with one click (the **Port** input + protocol picker at the top of the tab). The chat walks you through the safe + persistent way to do it.
- **Harden this server's firewall** flips a default-allow server to default-deny safely. A modal lists every service currently listening and lets you pick which should stay public. SSH is always kept (the safeguard refuses to lock you out). Docker is detected automatically and a Docker-compatible mode is used so containers keep working.
- **Diagnose unreachable** checks this layer (and the others) and reports which one is blocking your port.

> **Important: enabling a server firewall + Docker is tricky.** Plain `ufw enable` on a Docker host can break container networking — Docker manages its own firewall rules through a separate chain (`DOCKER-USER`), and `ufw` doesn't know about it. If you let Server Manager handle the harden, it detects Docker and uses a Docker-safe path automatically. Doing it by hand without that integration is a common way to lose connectivity to your containers.

## Layer 1 — The cloud-provider firewall (off the server)

The trickiest layer. Your hosting provider runs a firewall **in front of** your server — packets are filtered before they ever reach your VM. Server Manager can't see into this firewall directly because it's not on your server; only your provider's web console can change it.

Names and behaviors vary wildly:

| Provider | What it's called | Default behavior |
|---|---|---|
| **Hetzner Cloud** | Firewalls (optional, attachable per-server) | If no firewall is attached, all ports are open; if one is attached, only its rules are allowed |
| **Oracle Cloud (OCI)** | Security Lists (subnet) + Network Security Groups (per-VNIC) | Default Security List opens 22/tcp, may block everything else depending on the shape |
| **AWS EC2** | Security Groups | Default group blocks everything except 22/tcp from anywhere |
| **Google Cloud (GCP)** | VPC Firewall Rules | Default rules block most inbound traffic |
| **DigitalOcean** | Cloud Firewalls (optional) | If you don't create one, no filtering at this layer |
| **Vultr / Linode** | Firewall (optional, attachable) | If none attached, no filtering at this layer |

The thing to internalize: **the server firewall and the cloud firewall are two separate firewalls**. Both can block. Both can let through. You can open a port on the server side and still be blocked by the cloud side (very common). You can have nothing on the server side but be blocked by the cloud side (also common).

**How Server Manager helps:**

- Server Manager can't change cloud-firewall rules directly (it would need API keys to every provider), but **Faro knows the click-by-click steps for every major provider** and walks you through it. After opening a port on the server side, Faro asks "do you want me to walk you through opening it on the cloud side too?" and gives you exact instructions for your provider's web console.
- When you run **Diagnose unreachable** in the Firewall tab, the diagnostic identifies whether the cloud firewall is the blocker and emits the per-provider walkthrough automatically.
- When you **Harden this server's firewall**, Faro reminds you to also narrow the cloud-side firewall to match (otherwise you have an asymmetric setup — tight on the server, loose at the cloud).

## How Server Manager checks all three layers for you

The chat path: open Faro and say *"why can't I reach port 8080?"* The agent probes all three layers in order — service listening, server firewall, cloud firewall — and tells you which one is the cause. Then offers to fix it.

The UI path: **Server details → Firewall tab → Diagnose unreachable**. Type the port, click the button. Same three-layer probe, same diagnosis, same offer to fix.

![Server panel Firewall tab with the inline port input and "Diagnose unreachable" button highlighted, plus the list of currently allowed ports (SSH locked, others with Close)](/help/why-cant-i-reach-my-port/02-firewall-tab-diagnose.svg)

The result is always one specific layer named as the cause, plus a single proposed fix as a yes/no question. No multi-page generic checklists.

## Common scenarios

### "I deployed a database and can't connect from my laptop"

Almost certainly Layer 3 — the database is **listening on `127.0.0.1`** only. PostgreSQL, MySQL, MariaDB, Redis all default to localhost. They're running fine, you just can't reach them from outside.

The fix depends on what you want:

- **For app→database on the same server** (the normal case) — that's how it should be. Connect your app to `localhost:5432` (or whatever the port is) and you're done. No firewall changes needed. *In Server Manager:* nothing to do — Faro deploys databases this way by default.
- **For "my laptop → database" remote admin** — open a tunnel: `ssh -L 5432:localhost:5432 user@server` from your laptop, then connect your DB client to `localhost:5432` locally. Safer than exposing the DB to the internet. *In Server Manager:* ask Faro for the exact tunnel command — it knows your server's user and host.
- **For "I really do want this database publicly reachable"** — change the bind address in the DB config to `0.0.0.0`, restart the DB, then open the port in both the server firewall AND the cloud firewall. Make absolutely sure you've set a strong password first; exposing a DB to the public internet without strong auth is a common way to get compromised. *In Server Manager:* ask Faro to do all three steps (rebind the DB, open the server-firewall port via the Firewall tab, walk through the cloud-provider rule). Have Faro generate a strong password first if you haven't set one.

### "I opened the port in `ufw` and still can't reach it"

Layer 1 — the **cloud-provider firewall** hasn't been told. Server-side `ufw allow 8080/tcp` opens Layer 2; the packet still has to clear Layer 1 before it reaches your server. Open the matching rule in your cloud-provider console.

*In Server Manager:* ask Faro to walk you through the cloud-side rule for your provider — it'll give you click-by-click steps for Hetzner / AWS / Oracle / etc. without you having to look up the dashboard layout.

### "It worked yesterday, doesn't today"

A handful of common causes:

- **Your IP changed** — if your cloud firewall is narrowed to "my IP only" and your home internet got a new IP overnight (very common with most consumer ISPs), the new IP is now blocked. Widen to `0.0.0.0/0` temporarily, get in, then re-narrow.
- **The service crashed** — Layer 3 problem in disguise. Check `systemctl status <service>` or `docker ps`.
- **The server was hardened recently** — if someone enabled the server firewall without including the port you care about, that port is now blocked at Layer 2. Open the **Firewall tab** to see the current ruleset.

*In Server Manager:* type the affected port into the Firewall tab and click **Diagnose unreachable** — Faro probes all three layers and tells you which one regressed. Faster than guessing.

### "I can curl it from the server, not from outside"

That's the diagnostic dividing line: it tells you the service is fine and Layer 3 isn't the problem. Now it's either Layer 2 (server firewall) or Layer 1 (cloud firewall). Use **Diagnose unreachable** to narrow it down further.

*In Server Manager:* the **Diagnose unreachable** button on the Firewall tab is the right tool for exactly this case — it skips Layer 3 (since you've already proven it works) and tells you which of the two firewall layers is dropping the packet.

## Common questions

**Do I need a server firewall if my cloud provider has one?**

Yes-and-no, depending on your threat model. A cloud firewall covers the common case. A server firewall is useful when:

- **You run Docker** — containers can punch their own holes that bypass the cloud firewall in some setups. A server firewall catches container egress.
- **You forget to remove temporary cloud-firewall rules** — server firewall is the safety net.
- **You want to filter outbound traffic** — cloud firewalls usually focus on inbound; server firewalls can do both.

If none of those apply, a single well-configured cloud firewall is enough for most setups.

**Does Server Manager work without a server firewall?**

Yes. Server Manager doesn't require any firewall configuration to function. The Firewall tab is for **your** safety, not for connecting to Server Manager itself (Server Manager only needs SSH, port 22).

**Why is the Firewall tab telling me "Default-allow" when I've never set anything up?**

Because that's the actual state. Most fresh servers have no server firewall configured — every port is reachable at that layer. Whether anything actually reaches your service depends on Layer 1 (cloud firewall) and Layer 3 (whether the service is listening). The yellow banner is a heads-up that you might want to harden it.

**Is "Open port 22 / SSH" something I should ever close?**

No. Server Manager talks to your server only over SSH. Closing port 22 disconnects Server Manager and you'd need provider-console recovery (see [Recover when SSH stops working](/help/recover-when-ssh-stops-working)). The Firewall tab refuses to offer a Close button on the SSH row for this reason, and the harden modal locks the SSH row to "always keep open."

## What's NOT in scope here

- **Outbound traffic problems** (your server can't reach the internet) — that's a different setup: usually NAT, DNS, or the provider's outbound network. Ask Faro to diagnose.
- **HTTPS / TLS errors** — those are reachability succeeded, but the encryption handshake failed. Different problem space; check the [domains-https-email](/help) section.
- **DNS problems** ("my domain doesn't resolve to the server") — covered in [Point a domain here](/help). The packet hasn't even gotten to the question of firewalls yet — it doesn't know where the server is.
- **Specific app config** (which environment variable to set so your app listens on `0.0.0.0` instead of `127.0.0.1`) — varies by app. Ask Faro: it knows the common ones.

---

# How pricing works

URL: https://servermanager.dev/help/how-pricing-works
Category: Account & billing
Last updated: 2026-05-27

> Server Manager is free to try (no card required) and €5 per 30-day pass after that. One-time payments, no subscription, no auto-billing without your explicit click each time.

Server Manager's pricing is intentionally simple: a free trial to get started, then €5 for every 30-day pass you want. No subscription, no surprise charges, no card required upfront.

## The free trial

New accounts normally start with a one-time **free trial** — enough budget to connect a server, run a few checks, deploy a small site, and decide if Server Manager works for you. (Occasionally new signups go straight to a 30-day pass instead.)

- **No card required.** Sign in with Google, Microsoft, or a magic link, that's it.
- **No time limit.** Use the trial at your own pace. Sign up today, come back in three months — the trial credit is still there.
- **Honest cap.** Once the trial budget is used, the chat pauses. You're never silently charged.

The free trial is a one-time grant: deleting and re-creating your account doesn't reset it.

## The 30-day pass — €5

When you're ready to use Server Manager for real work, **€5 buys you a 30-day pass**:

- Roughly **340,000–785,000 input tokens + ~44,000 output tokens** of LLM usage per pass, depending on how much prompt caching kicks in. Most users finish well under this.
- **30 days from purchase.** Pass starts the moment you pay; expires 30 days later regardless of how much you've used.
- **One-time payment.** Each pass is a separate purchase. You're not on a recurring subscription.

When the pass expires or its budget runs out, you simply buy another one when you next need to work on your server.

## What happens when the pass runs out before 30 days

Two paths:

1. **Manual:** the chat pauses, you see a "Get a pass" prompt, you go to **Pricing** and buy another €5 pass. Takes about 30 seconds.

2. **Auto-refill (opt-in):** if you've saved a card and turned on auto-refill, a one-click prompt appears in the chat. Tap **OK** and you keep working — no checkout, no waiting. See [Auto-refill](/help/auto-refill) for the full flow.

Either way, you decide. Auto-refill never charges your card without your explicit click each time.

## What happens at the 30-day mark

If you still have budget left after 30 days, it **expires** when the pass ends. Buying another pass starts a fresh batch — the previous balance doesn't carry over.

This is why you'll see a small warning on `/pricing` if you try to buy a new pass while your current one is still active and has budget left: **you'd lose what's unspent.** Server Manager won't let you accidentally waste money — wait until your current pass is exhausted or expired, then buy again.

## Free tier vs paid pass — what's included

| | Free trial | €5 pass |
|---|---|---|
| Approx. usage included | ~51k–118k input + ~6.6k output tokens | ~340k–785k input + ~44k output tokens |
| Time limit | None | 30 days from purchase |
| Card required | No | Yes |
| Refills | Single grant only | Buy another pass any time |
| Auto-refill | Not available | Available (opt-in) |

Ranges reflect how much *prompt caching* kicks in (responses to similar prompts cost roughly 10× less). Output tokens are billed at 6× the rate of input, so a single very long agent response can burn through more budget than a long chat.

## Why €5

Each €5 pass is sized so that:

- The LLM costs we incur per heavy user fit comfortably under the price
- Most users finish at 5–20% of their pass budget, so there's healthy margin on average
- There's no need for a higher-priced tier — features work the same for everyone

If a future model change shifts cost dramatically, we'd adjust pricing transparently, not silently.

## How to pay

From the avatar menu in the top-right corner of the app, click **Pricing**. The page walks you through the pass purchase, including:

- An optional "Save my card for one-tap auto-refill" checkbox
- A consent box you must tick (it's the EU Art. 16(m) right-of-withdrawal waiver — see the [Terms of Service](/legal/terms))

Payments are handled by **Stripe**. We never see your card number.

## Refunds

Short version: **full refund if you ask within 14 days of purchase AND you haven't used any tokens.** Once tokens are consumed, no refund. See [Manage billing, invoices & refunds](/help/manage-billing-invoices-refunds) for the long version.

---

# What Server Manager stores about you

URL: https://servermanager.dev/help/what-we-store-about-you
Category: Privacy & data safety
Last updated: 2026-05-27

> A plain-English inventory of every piece of data we hold — your email, your server connection metadata, billing records, usage counts. What we never store: SSH passwords/keys in plaintext, chat history past your session.

This is the practical version of our [Privacy Policy](/legal/privacy) — plain English, no legal-speak, organized by "what is this exactly and why does Server Manager need it".

## Account information

Set when you sign up.

- **Email address** — used to identify your account and send authentication links
- **Name** — only if you signed in with Google or Microsoft (populated from that profile); never required
- **Authentication provider** — which login method you used (Google, Microsoft, or email magic link)
- **Account created date** — for our records and yours

## Saved server profiles (opt-in)

When you save a connection profile so you don't have to re-enter credentials every session.

- **Alias** (your label, e.g. "Prod OCI")
- **Host / IP address**
- **SSH port** (usually 22)
- **Username**
- **Encrypted credential blob** — the SSH password or private key (encrypted with AES-256-GCM, using a key derived from your passphrase via scrypt). **Your passphrase is never stored** — see [How SSH credentials are handled](/help/how-ssh-credentials-are-handled) for the full mechanism.

You can opt out of saving credentials and just save the alias — useful if you want a bookmark without the security exposure.

## Active session data (in-memory only)

When you're connected to a server.

- **SSH session credentials** — the actual password or unwrapped private key needed to talk to your server. **Held only in process memory while the session is open. Never written to disk. Discarded the moment you disconnect or your session times out.** This is the most security-sensitive data and it's specifically *not* stored.

## VPS claim records

To prevent the "create a new account to drain free trial on the same server" abuse pattern.

- **Host you connected to** (IP or hostname, lowercased)
- **First-claimed timestamp** + last-activity timestamp

One active claim per host across all users at a time. See [How we prevent abuse](/help/can-i-use-my-existing-vps) for the full picture.

## Billing data

Handled by **Stripe**, with a small mirror on our side.

- **Stripe customer ID** — links you to your records on Stripe's side
- **Stripe payment method ID** — only if you saved a card for one-tap auto-refill; this is an ID, not a card number
- **Pass start / end dates**
- **Cumulative LLM cost on the current pass** — used by the in-app usage bar
- **Per-purchase consent log** — timestamp, IP, user-agent, and which version of the Terms / Privacy Policy you accepted at each purchase (legal audit trail for the EU Art. 16(m) waiver)

We **never see your card number**. Stripe handles that end-to-end.

## Usage logs

To bill correctly and detect abuse.

- **Per-LLM-call records**: tokens used (input, cached input, output), the model name, the calculated cost in EUR, a session ID, an optional recipe identifier. **No chat content is included.**

Used to drive the usage bar on your Account page and for aggregate anonymized reporting.

## Free-tier anti-abuse data

- **Free-tier grant** (email + grant date + optional revocation date)
- **Observed VPS hosts** linked to your grant — anonymized as just IP/hostname strings; used for the 90-day cross-account match check

## File snapshots (Undo)

When Faro is about to overwrite a config file, it captures the current contents so you can Undo.

- **The shell command that triggered the snapshot**
- **Each affected file path + the file's bytes as base64** (so we can put it back if you click Undo)
- **The session ID and the host name** so we know where to write back

Snapshots are auto-deleted 30 days after capture by a cleanup job. They never include passwords or secrets *unless your config file already contained those in plaintext* — which it shouldn't (Server Manager always reads from `.env` / secrets-mount files, not from chat input).

## What we explicitly do NOT store

- **Your chat messages with Faro after the session ends.** Sessions are RAM-only by design. Close the tab → conversation is gone.
- **Your SSH passwords or private keys in plaintext.** Either encrypted with a passphrase only you know, or held only in process memory for the duration of the session.
- **Card numbers, expiry dates, CVVs.** Stripe handles those.
- **Your IP address for analytics / fingerprinting.** We capture it once per purchase consent (legal audit trail) and never combine it with behavioral data.
- **Analytics or marketing cookies.** Only the authentication session cookie is set.

## Where this data physically lives

- **Database**: hosted on Neon (PostgreSQL, EU region)
- **Application**: hosted at our chosen infrastructure provider
- **Billing data**: Stripe (Ireland EU entity for European customers; US transfers covered by Standard Contractual Clauses)
- **Email delivery**: Resend (US, SCCs)
- **LLM provider**: OpenAI (US, SCCs)

For the legally-binding wording, see the [Privacy Policy](/legal/privacy). Anything missing or unclear, email support@servermanager.dev.

---

# Do I need to know Linux to use Server Manager?

URL: https://servermanager.dev/help/do-i-need-to-know-linux
Category: Get started
Last updated: 2026-05-24

> No. Server Manager is designed for people who don't want to live in a terminal. You'll see commands, but Faro explains them in plain English and runs them for you after you approve.

No. Server Manager is designed for people who don't want to live in a terminal.

You will see commands (it's a transparency choice — you should always know what's about to happen to your server), but Faro explains what each one does in plain English before running it, and you approve every command that changes anything. Read-only commands run automatically; anything destructive pauses for your "yes".

If something needs your attention outside of Server Manager — like adding a DNS record at your domain registrar, or opening a cloud firewall port — Faro gives you provider-specific, click-by-click steps. No assumed knowledge.

That said: knowing **what a domain is**, **what a server is**, and **what HTTPS is** will help. We assume that level of context, not Linux command-line fluency.

---

# Deploy a web app from your computer

URL: https://servermanager.dev/help/deploy-web-app-from-your-computer
Category: Deploy & manage sites and apps
Last updated: 2026-05-23

> Drop your Node, Python, or Go project folder into Server Manager, type your domain, click Deploy.

> Have your code on a git repo? → [Deploy a web app from a git repo](/help/deploy-web-app-from-a-git-repo)

> Have a static website (HTML/CSS/JS) instead? → [Deploy a website from your computer](/help/deploy-website-from-your-computer)

A web app is code that runs on the server and answers requests — a Node.js API, a Python Flask app, a Go HTTP server, etc. For a folder of plain HTML/CSS/JS, see the static-site article above.

## 1. Open the Actions menu

In the top bar, click **Actions**. In the palette that opens, choose **Deploy from my computer** (under "Bring something in") — or just type "deploy" in the search box.

![Click Actions in the top bar, then choose Deploy from my computer](/help/deploy-web-app-from-your-computer/01-set-up-menu.svg)

## 2. Pick "Web app"

A new window opens. Click the **Web app** tab.

![The Deploy window opens — click the Web app tab](/help/deploy-web-app-from-your-computer/02-web-app-tab.svg)

## 3. Drop your project folder

Drag the folder that contains your app's source code into the dashed box. We detect the runtime (Node, Python, or Go) from files like `package.json`, `requirements.txt`, or `go.mod`.

![Drag your project folder into the dashed drop zone](/help/deploy-web-app-from-your-computer/03-drop-folder.svg)

## 4. Type your domain

Type the address you want the app to live at — for example, `api.example.com`. You can leave it blank for now and add a domain later.

![Type your domain in the Domain field](/help/deploy-web-app-from-your-computer/04-domain.svg)

## 5. Click Deploy

The button at the bottom shows how many files will be uploaded. Click it.

![Click the Deploy button at the bottom-right](/help/deploy-web-app-from-your-computer/05-deploy.svg)

## 6. Done

The window closes and the chat takes over. You'll see each step as it happens: we install the runtime if needed, start your app under [[systemd]], and point [[caddy|Caddy]] at it as a [[reverse-proxy]]. When it finishes, your app appears on the home screen with a green dot and the runtime it's using.

![Your new web app appears in the overview with a green dot](/help/deploy-web-app-from-your-computer/06-done.svg)

## Want to fine-tune things first?

Before clicking **Deploy**, expand the **Advanced** section in the window. Every field below is pre-filled from what we detected — override any of them.

![The Advanced section expanded — runtime, deploy mode, app name, port, start command, env vars](/help/deploy-web-app-from-your-computer/07-advanced.svg)

**Runtime.** We pick Node, Python, or Go automatically from your project files. Override it here if our detection got it wrong (or you want a specific one).

**Deploy as.** **Native process** runs your app directly on the host under [[systemd]] — the fastest option, lowest overhead, but every Native app on this server shares the same runtime version. **Container** runs your app inside Docker, so it pins its own runtime version (e.g., Node 18 alongside Node 22) at the cost of about 30–100 MB extra RAM. Full breakdown in [Native vs Container](/help/native-vs-container).

**App name, port, start command.** The app name becomes the folder under `/opt/`, the [[systemd]] unit name, and the dedicated app user. The port is what your app listens on internally — [[caddy|Caddy]] reverse-proxies your domain to it. The start command is what [[systemd]] runs to launch the app (`npm start` for Node, `python app.py` for Python, `./server` for Go by default).

**Environment variables.** Expand the **Environment variables** disclosure to add lines like `DATABASE_URL=…` or `API_KEY=…` — one `KEY=VALUE` per line. They're stored in `/etc/<app>/env` (root-owned, mode 600) and loaded by [[systemd]] at launch. Don't commit these to your git repo.

---

# Auto-refill — never get stuck mid-task

URL: https://servermanager.dev/help/auto-refill
Category: Account & billing
Last updated: 2026-05-27

> Save a card and turn on auto-refill. When your tokens run out, a one-click prompt asks you to authorize another €5. You stay on the same screen and keep working. Every charge needs your explicit click — no silent billing.

Auto-refill is the answer to "I'm in the middle of a task and just ran out of tokens." Instead of being kicked to a checkout page mid-flow, you get a one-click prompt in the chat. Tap **OK**, and a fresh 30-day pass is on your account in about a second.

## The shape of it (in one paragraph)

You save a card on your account → you turn auto-refill on → when your tokens run out, a modal appears asking "Authorize €5 for another pass?" → you click OK → Server Manager charges your saved card and grants a new pass → the modal disappears and your chat resumes. **Each refill needs your click.** Auto-refill never charges your card silently.

## Why we made it work this way

Two design choices that protect you:

1. **Explicit consent every time.** Many subscription products silently charge your card whenever they want. Auto-refill in Server Manager doesn't: even though your card is saved, no charge happens without an in-app OK click from you. This is deliberate — it prevents disputes, gives you full control, and means you can always say "not now" without consequences.

2. **No monthly cap.** You set the rhythm. There's no "you've spent €X this month — please confirm before continuing" interruption. Since every charge already requires your OK click, an extra cap would just nag you about decisions you're actively making.

## How to turn auto-refill on

**Easiest path — at purchase:** when you buy your first pass on the [Pricing](/pricing) page, tick the **"Save my card for one-tap auto-refill"** box. Card gets saved by Stripe, auto-refill flips on. Done.

**Or later — from your Account page:** click your avatar in the top-right → **Account** → scroll to the **Auto-refill** section → click **Save a card**. Stripe Checkout opens in setup mode (no charge happens). Enter your card details, submit, and you're returned to the account page with auto-refill on.

## How a refill actually happens

You're chatting with Faro, deploying a site, debugging a server. Your pass budget hits zero. Within a few seconds:

1. The chat pauses on the next agent action.
2. A modal pops up:
   > **Pass used up — refill?**
   > You've used the full budget on your current pass. Authorize another **€5** for a fresh 30 days? Your saved card will be charged on the spot.
   >
   > **[OK, refill €5]   [Not now]**
3. You click **OK, refill €5**.
4. The button changes to "Charging…" briefly (about 1–2 seconds).
5. A "Refill done — your pass is topped up. The agent is resuming…" message appears.
6. The modal closes; the usage bar resets; Faro continues from where it stopped.

If you click **Not now** instead, the modal closes and the chat stays paused. You can refill later from `/pricing` or click your avatar → Pricing.

## What you see in the Billing section

When auto-refill is on, the Account page shows:

> **Auto-refill is ON.** A saved card is on file. You'll be prompted to authorize each charge.
> **[Turn off]**

When it's off but you still have a saved card:

> **Auto-refill is OFF.** Your saved card is still on file. Turn auto-refill on to use it.
> **[Turn on]**

When you've never saved a card:

> **Auto-refill is OFF.** No card on file. Save one to enable.
> **[Save a card]**

## Edge cases

**Card declined.** If the refill charge fails (insufficient funds, expired card, etc.), the modal shows the decline message and offers a "Try again" button. Update the card in Stripe's customer portal (via **Manage billing** on your Account page) and retry.

**3D Secure / strong customer authentication.** EU regulations sometimes require your bank to re-verify the charge. If that happens, Server Manager surfaces a message telling you to update your card via Manage billing. (Future improvement: handle the 3DS prompt directly in the modal.)

**Multiple refills in a short time.** A 5-second idempotency window prevents accidental double-click charges. Beyond that, repeated refills are allowed — each one needs its own OK click.

## Turning auto-refill off

One click. On your Account page, in the Auto-refill section, click **Turn off**. The flag flips immediately. Your saved card stays on file (so you can flip back on later without re-entering card details). To fully remove the card, use **Manage billing** to open Stripe's customer portal and detach the payment method there.

## What auto-refill is NOT

- **Not a subscription.** No monthly billing, no recurring charge, no calendar-based renewal. Each refill is a discrete €5 payment triggered by token exhaustion *and* your click.
- **Not silent.** You always see and authorize each charge.
- **Not required.** You can always pay manually one pass at a time. Auto-refill just makes mid-task refills frictionless.

## When auto-refill doesn't fire

Auto-refill triggers **only when you run out of tokens inside a still-time-valid 30-day pass.** If your pass *time-expires* (30 days passed with tokens still left), you're not charged automatically — those tokens were yours, you'd have lost them either way, and we don't fire a charge to claim something you weren't actively using. Instead, you'll see the regular "Get a pass" prompt the next time you open the app.

If you want to plan ahead before that happens, just buy another pass manually from [Pricing](/pricing) — but note that buying with tokens still on the current pass would forfeit those tokens. Server Manager warns you about this and disables the buy button until your current pass is exhausted.

---

# How your SSH credentials are handled

URL: https://servermanager.dev/help/how-ssh-credentials-are-handled
Category: Privacy & data safety
Last updated: 2026-05-27

> SSH passwords and private keys live only in process memory while a session is open — never on disk. Saved server profiles encrypt credentials with a passphrase only you know. Server compromise leaks ciphertext only; the attacker still needs to brute-force your passphrase per server.

This is the most security-sensitive part of Server Manager. Here's exactly what happens to your SSH credentials at every stage.

## When you connect a server (no saving)

The default path: type your server IP, username, and password/private-key into the [Connect modal](/help/can-i-use-my-existing-vps), click Connect.

- Your credentials are sent over **HTTPS** to Server Manager
- They're held in **process memory** of the server-side session that talks to your VPS
- **Nothing is written to disk.** No database, no log file, no cache.
- When you disconnect, close your browser tab, or the session times out, the credentials are discarded from memory

If our server is rebooted, your credentials are gone — you'd need to reconnect manually next time. This is by design.

## When you save a server profile (opt-in)

If you tick "Save this server" during connection, we encrypt your credentials with a passphrase only you know.

### What gets encrypted

The encrypted blob contains your SSH password OR your private key + optional passphrase for the key. Plus a small metadata header so we know which credential type to use.

### How encryption works

- You provide a passphrase at save time (we recommend something you'd use for nothing else)
- The passphrase is fed through **scrypt** — a deliberately slow password-based key derivation function — with a per-server random 16-byte salt, to produce a 256-bit AES key
- Your credentials are encrypted with **AES-256-GCM** (authenticated encryption — tampered ciphertext fails to decrypt, no padding-oracle attacks)
- We store the encrypted ciphertext, the salt, the GCM nonce, and the GCM authentication tag in the database
- **Your passphrase is never stored.** Not hashed, not derived, not present anywhere on our side.

### How we tell "wrong passphrase" from "tampered data"

Both fail the same way: AES-GCM authentication-tag check fails. There's no stored verifier we can compare your passphrase against, no hash to leak. Wrong passphrase = decryption error = you re-enter and try again.

### When you reconnect to a saved server

You paste your passphrase. We re-derive the key with scrypt + the stored salt, decrypt the credential blob, use the decrypted credentials to open the SSH session, then **immediately discard the decrypted credentials from memory** once the session is established.

The decrypted SSH credentials live only in the session-handling code for the lifetime of the SSH connection itself. Same as the no-save flow above — memory-only, no disk.

## What happens if our server is compromised

This is the threat we designed against. Concretely:

- Attacker gets **read access to the database**: they get ciphertext. To use any saved credential, they need to brute-force the passphrase per server.
  - scrypt is intentionally slow — every passphrase attempt takes ~100ms+ of CPU time on modern hardware. Brute-forcing even a 6-character random passphrase takes years on a single machine, decades on commodity GPUs.
- Attacker gets **full server compromise** (RCE — remote code execution — during a live session): they could read decrypted credentials for users currently online. But they don't get access to saved-but-offline users' credentials.
- Attacker gets **a database backup from last week**: same as read access — they have ciphertext only.

### What this means for you, in practical terms

The three scenarios above translate to very different real-world exposure:

- **Database read or stolen backup (the most common kind of breach):** the attacker gets encrypted blobs and nothing else. To actually use any saved credential they'd have to brute-force your passphrase, which scrypt makes infeasible for a strong one. **Your servers stay safe.**
- **Full server compromise while you're actively connected (rare and severe):** because an open SSH session needs your real credentials in memory, an attacker controlling our running process in that window could extract the decrypted credentials of *users who are connected at that moment* and use them to log into those users' actual servers. **If you're offline at the time, your saved credentials are still just ciphertext to them — you're not affected.**

The key property: even in the nightmare full-compromise case, the blast radius is limited to "whoever happens to be online during the attack," not "everyone who ever used Server Manager." Most catastrophic breaches expose every user's secrets at once; this design deliberately doesn't.

| Breach type | What the attacker gets |
|---|---|
| Database read (most common) | Ciphertext only — nobody's usable credentials |
| Stolen DB backup | Ciphertext only |
| Full live-session RCE (rare, severe) | Decrypted credentials of *only* users connected in that window |

This is meaningful protection against the most common breach pattern (data exfiltration), and it deliberately accepts that an attacker with full live-server compromise is a different, harder threat we don't claim to fully defeat — while still bounding even that worst case to currently-online users.

## Strong-passphrase advice

The whole encryption scheme is only as strong as your passphrase. Recommended:

- **At least 20 random characters**, or
- **Five random English words** (à la "correct horse battery staple") — easy to remember, hard to brute-force
- **Never reuse** the passphrase you use for your laptop login, your password manager, or your email
- **Don't share it.** Server Manager itself doesn't need it — it's only between you and the encrypted blob.

If you lose the passphrase, the saved credentials are unrecoverable. You'd delete the saved server profile and re-add it from scratch.

## Why we don't offer "passwordless" or "convenience" key access

A few products store SSH keys with no encryption ("trust us, we keep them safe") or behind only the user's account password. Server Manager doesn't. The passphrase mechanism is the line between "anyone with our database can SSH into your servers" and "anyone with our database needs to brute-force your passphrase first." We think that's worth the one-time inconvenience of choosing a passphrase.

## What's NEVER sent or stored

- **SSH private keys in plaintext** to our database
- **SSH passwords in plaintext** to our database
- **Your encryption passphrase** — anywhere, in any form
- **Decrypted credentials in any persistent storage** — disk, logs, backups, cache

The only place decrypted credentials exist is in the live session's process memory, and only for the duration of the session.

## Questions or concerns?

If anything in this article is unclear, or you have a specific security question about how your credentials are handled, reach out via [Contact](/help/contact) — we're happy to explain in more detail.

---

# Which Linux distros work with Server Manager?

URL: https://servermanager.dev/help/which-linux-distros-work
Category: Get started
Last updated: 2026-05-24

> Ubuntu and Debian are the primary targets. Fedora, Rocky, Alma, and openSUSE Leap work for most recipes — some edges still rough.

**Primary**: Ubuntu (22.04, 24.04) and Debian (12). All recipes are tested against these.

**Working with minor edges**: Fedora, Rocky Linux, AlmaLinux, openSUSE Leap. Most recipes work, but some assume Debian-family conventions (e.g., package names, default `dig` availability) and may need a one-click follow-up. We're closing these gaps over time.

**Not supported**: Windows Server, FreeBSD, Alpine (no systemd), or any distro without sudo + SSH access for a user in the docker group (or convertible to one).

If your VPS provider only offers an unusual distro, drop us a line — adding support is mostly a matter of mapping the package manager and a few path conventions, and we prioritize based on demand.

---

# Deploy a website from a git repo

URL: https://servermanager.dev/help/deploy-website-from-a-git-repo
Category: Deploy & manage sites and apps
Last updated: 2026-05-23

> Paste your repo URL, type your domain, click Clone & deploy.

> Don't have a git repo? → [Deploy a website from your computer](/help/deploy-website-from-your-computer)

If your website lives in a public git repo (GitHub, GitLab, Bitbucket, Codeberg, Gitea — any HTTPS git host), Server Manager can clone it directly onto your server. Updates later are a one-click [Pull latest from git](/help/pull-latest-from-git) — no re-upload needed.

## 1. Open the Actions menu

In the top bar, click **Actions**. In the palette that opens, choose **Deploy from my computer** (under "Bring something in") — or just type "deploy" in the search box.

![Click Actions in the top bar, then choose Deploy from my computer](/help/deploy-website-from-a-git-repo/01-set-up-menu.svg)

## 2. Switch the Source to "From a git repo"

A new window opens. Under **Source**, click **From a git repo**. The drop zone disappears and a URL field appears in its place.

![Switch the Source to From a git repo](/help/deploy-website-from-a-git-repo/02-source-git.svg)

## 3. Paste your repo URL

Paste the `https://` URL of your repo — for example, `https://github.com/yourname/mysite`. Use the HTTPS form, not the `git@` SSH form.

![Paste your repo URL into the field](/help/deploy-website-from-a-git-repo/03-repo-url.svg)

## 4. Type your domain

Type the address you want the site to live at — for example, `mysite.example.com`. You can leave it blank to publish at your server's IP for now.

![Type your domain in the Domain field](/help/deploy-website-from-a-git-repo/04-domain.svg)

## 5. Click "Clone & deploy"

![Click Clone & deploy at the bottom-right](/help/deploy-website-from-a-git-repo/05-deploy.svg)

## 6. Done

The window closes and the chat takes over. We clone your repo onto the server, set up [[caddy|Caddy]], request a [[lets-encrypt|Let's Encrypt]] certificate, and reload the proxy. When it finishes, your site appears on the home screen with a green dot — and a **[Pull latest from git](/help/pull-latest-from-git)** button appears in its service panel.

![Your new site appears in the overview with a green dot](/help/deploy-website-from-a-git-repo/06-done.svg)

## Updating later

Click **[Pull latest from git](/help/pull-latest-from-git)** in the site's service panel — we'll `git pull` your latest commits without re-cloning. For private repos, generate a deploy key from the same panel; pulls won't ask for credentials again. The full breakdown (conflict handling, deploy keys, monorepo subdir behavior) lives in [the Pull latest article](/help/pull-latest-from-git).

## Want to fine-tune things first?

Before clicking **Clone & deploy**, expand the **Advanced (git)** section.

![The Advanced (git) section expanded — Branch, Subdirectory, Personal access token](/help/deploy-website-from-a-git-repo/07-advanced-git.svg)

**Branch.** Override the default branch — anything `git clone --branch` accepts, including tags and commit SHAs.

**Subdirectory.** For monorepos: deploy just one folder inside the repo (e.g., `apps/web`). We use sparse-checkout so **Pull latest** keeps working — only your subdirectory gets materialized but the full `.git` history is there for future pulls.

**Personal access token.** For private repos. We use the token once for the initial clone, then scrub it from the local remote URL immediately after. After the deploy finishes, generate a **[deploy key](/help/pull-latest-from-git#for-private-repos-generate-a-deploy-key-first)** from the service panel for long-lived SSH-based pulls.

---

# Invoices, refunds, and "cancelling"

URL: https://servermanager.dev/help/manage-billing-invoices-refunds
Category: Account & billing
Last updated: 2026-06-20

> Find your receipts via Stripe's customer portal — these are receipts, not Italian fatture. Need a fattura elettronica? Tick "I need an invoice" at checkout, or email support@servermanager.dev for a past purchase. Refund policy is no-refund-except-zero-tokens-used-within-14-days. There's nothing to "cancel" — Server Manager has no subscription and never charges without a click (auto-refill included), so you just stop using it.

The practical billing stuff: where to find your receipts, when you can get a refund, and how to stop being charged.

## Where to find your receipts

We use **Stripe** for payments. Stripe generates a receipt PDF for every successful charge and keeps a permanent record of all your transactions on Server Manager.

To access them: click your avatar → **Account** → scroll to the Billing section → click **Manage billing**. This opens Stripe's customer portal, where you can:

- See every past payment with date, amount, and description
- Download PDF receipts (useful for accounting or expense reports) — note these are receipts, not Italian *fatture* (see the next section)
- Update your payment method (add, change, or remove cards)
- See your billing address and email

If you've never paid (you're on the free trial), the **Manage billing** button doesn't appear — there's nothing to manage yet.

## Receipt vs. invoice: which one you get

These are two different documents, and the difference matters mostly if you're an Italian business:

- **The PDF you download from Stripe is a *receipt*** — proof that your payment went through. For personal use, or for any customer outside Italy, that receipt is all you need for your records.
- **A *fattura elettronica* is the formal Italian tax invoice**, issued through Italy's SdI system (*Sistema di Interscambio*). Stripe receipts are **not** fatture and are not sent through SdI — so a downloaded Stripe PDF, on its own, is not a valid Italian invoice.

If you need a proper **fattura** (typically Italian businesses or VAT-registered freelancers), there are two ways to ask for one:

- **At checkout (the easy way):** tick **"I need an invoice"** on the purchase screen *before you pay* and fill in your fiscal details. Italian law lets you request the invoice up to the moment of purchase, so this is the cleanest path.
- **For a past purchase:** email **support@servermanager.dev** from your account email address and we'll issue the *fattura elettronica* by hand. To get it done in one go, include:
  - Full name (private) or *ragione sociale* (business)
  - *Codice fiscale* (private) or *partita IVA* (business / VAT-registered)
  - Full address — street, CAP/postal code, city, and *provincia* for Italy
  - Country
  - Businesses only: *codice destinatario* (7 characters) **or** a PEC address
  - Which purchase it's for (the date and amount, or your Stripe receipt)

Either way, we build the *fattura elettronica*, send it through SdI, and email you a copy.

> **In italiano** — Ti serve la **fattura** per un acquisto? Il modo più semplice è spuntare **"Ho bisogno di una fattura"** al momento del pagamento. Se l'acquisto è già stato fatto, scrivi a **support@servermanager.dev** dall'indirizzo email del tuo account, indicando: nome e cognome o ragione sociale; *codice fiscale* o *partita IVA*; indirizzo completo (via, CAP, città e provincia); per le aziende il *codice destinatario* (7 caratteri) oppure un indirizzo PEC; e a quale acquisto si riferisce (data e importo). Emetteremo la *fattura elettronica*, la invieremo allo SdI e te ne invieremo una copia via email.

## EU VAT

Server Manager is run by an Italian sole trader under the *regime forfettario*. **No VAT is charged** on any pass — the price you see (€5) is the total you pay, with no tax added on top.

## Refund policy

Short version: **full refund if you haven't used any tokens and you ask within 14 days — once you've used any tokens, no refund.**

**When you qualify:** if you have not used any tokens from the pass AND you request the refund within 14 days of purchase, we'll refund you in full. The fastest way is the **Withdraw from contract here** button on your Account page, under *Right of withdrawal* — it processes the refund automatically and emails you a confirmation. You can also email support@servermanager.dev from your account address; either way it's handled within a few business days.

**Why the policy works this way.** When you tick the consent box at checkout, you ask us to make the service available immediately and acknowledge (Art. 16(m) of the EU Consumer Rights Directive) that you give up your 14-day right of withdrawal once you actually use the service. So as long as you haven't used any tokens, that right still stands — request a refund within 14 days and we'll refund you in full. The moment you use any tokens, performance has begun at your request and the refund window closes.

**What doesn't qualify for a refund:**

- "I used some tokens but want my money back" — no
- "I cancelled but already used tokens" — there's nothing to cancel; tokens used = no refund

**On auto-refill specifically:** you'll never be charged for a refill you didn't approve. Every auto-refill requires you to click **OK** in the app at the moment your tokens run out — a charge can't happen without your active consent. There's no silent or background billing to be surprised by, so the "I didn't realize I'd be charged" scenario simply can't arise.

## "How do I cancel?"

There's nothing to cancel. Server Manager doesn't have a subscription — every €5 pass is a one-time payment, and **you're never charged without actively clicking to pay**. Even auto-refill needs you to click **OK** each time your tokens run out, so there are no automatic future charges to stop, whether or not auto-refill is on.

A few related things you might actually want:

1. **Just stop using it**: do nothing. Your current pass runs until its tokens are used or 30 days pass, then access stops until you choose to buy again. Nothing is charged in between.

2. **Stop seeing the auto-refill prompt**: if you'd rather not be offered a one-click refill when your tokens run out, turn auto-refill off on your Account page. This is optional — with it on you're still only ever charged when you click OK; turning it off just removes the prompt.

3. **Delete your account entirely**: scroll to the Delete account section on your Account page. Account data is held for 30 days in case you change your mind, then permanently erased. See [the privacy section](/help/category/privacy-data-safety) for what data is held and for how long.

## Updating your payment method

Open Stripe's customer portal via **Manage billing**. Add a new card, remove an old one, change the default. Changes apply immediately — if auto-refill is on, the next refill uses the new default card.

## Failed payments

If an auto-refill charge fails (declined card, expired card, insufficient funds, etc.), the refill modal shows the decline reason. Server Manager won't repeatedly retry — you'll need to update your card via Manage billing and then trigger the refill again.

We don't have dunning emails or grace periods because there's no subscription. Failed charge = the refill simply didn't happen; your access stays paused until you fix the card OR manually buy a fresh pass.

## Where this all lives in the app

| What you want | Where it is |
|---|---|
| Buy a pass | Click your avatar → **Pricing** |
| See your current pass status | Click your avatar → **Account** → Billing section |
| Toggle auto-refill on/off | Click your avatar → **Account** → Auto-refill section |
| See past receipts, update card | Click your avatar → **Account** → Billing → **Manage billing** (Stripe portal) |
| Request a *fattura elettronica* | Tick **"I need an invoice"** at checkout — or, for a past purchase, email support@servermanager.dev |
| Withdraw / request a refund | Click your avatar → **Account** → **Right of withdrawal** → **Withdraw from contract here** (or email support@servermanager.dev) |
| Delete your account | Click your avatar → **Account** → Delete account section (scroll to bottom) |

---

# What Faro sees in your chats

URL: https://servermanager.dev/help/what-faro-sees-in-your-chats
Category: Privacy & data safety
Last updated: 2026-05-27

> Your chat messages + relevant server context (inventory output, file contents Faro reads) are sent to OpenAI to generate responses. Chat content isn't stored on our side past your session — it's in-memory only. OpenAI doesn't train on your data per their API policy.

Faro is the AI agent that powers Server Manager — the thing you chat with. Behind the scenes, Faro is a Large Language Model from OpenAI. Here's what that actually means for your data.

## What gets sent to OpenAI on every message

Each time you send a message to Faro, the following is bundled and sent to OpenAI's API:

1. **Your message text** — the prompt you just typed
2. **A system prompt** — Server Manager's instructions to Faro (tool definitions, behavior guidelines, recipe knowledge). Same for everyone.
3. **The recent conversation history** — your previous messages and Faro's responses, within the session
4. **Server context Faro has gathered**, when relevant:
   - Output from the inventory script (running services, Docker containers, recent system events)
   - Contents of config files Faro has read (`/etc/nginx/sites-enabled/...`, Caddyfile, `.env` files, etc.) — *but only when needed for the specific task*
   - Output from commands Faro ran on your server during this session
   - Inferred state about your server (distro, package manager, web engine, etc.)

OpenAI uses this to generate Faro's next response.

## What OpenAI does with this

Per [OpenAI's API data usage policy](https://openai.com/api-data-privacy):

- **They don't train on API data by default.** Your messages aren't used to improve future models.
- They retain API request/response logs for **up to 30 days** for abuse-monitoring (then deleted), with some narrow exceptions if abuse is suspected.
- They have SOC 2 Type 2 and ISO 27001 certifications.
- For EU users, transfers to OpenAI (US) are covered by **Standard Contractual Clauses (SCCs)** under GDPR.

Server Manager uses OpenAI's standard API, not their consumer ChatGPT product. The terms above are the API terms.

## What Faro can't see

- **Your SSH credentials.** Even though Faro runs commands on your server, the credentials live in process memory of Server Manager's session code, not in the prompt sent to OpenAI. Faro talks to your server *through* Server Manager; OpenAI doesn't get the keys.
- **Other users' conversations or servers.** Each chat session is isolated; nothing is shared between accounts at the prompt level.
- **Your encryption passphrase** (the one for saved server profiles). Never sent to OpenAI; not visible in the chat context.

## What about secrets in config files?

Faro reads config files when needed (e.g., to diagnose why a domain isn't serving HTTPS, it might read your Caddyfile). If those files contain secrets — API keys, database passwords — those secrets *are* part of the prompt sent to OpenAI for that turn.

Two mitigations:

1. **Faro is instructed to never echo secrets back to you** in plain text in its response. It uses placeholders like `<redacted>` when summarizing. *(This is a soft mitigation — LLMs can occasionally leak; treat any secret Faro saw as having transited OpenAI's API and apply your own policy.)*
2. **Server Manager's recipes always read secrets from secrets files, never from chat input.** When a recipe needs to inject a value into a config, it generates one server-side and writes it to the appropriate file, not via prompting the user to type it.

If you're particularly concerned about a specific file, you can tell Faro "don't read /path/to/file" and it will respect that.

## Chat history retention on our side

**We don't persist chat history.** Sessions are RAM-only by design — a JavaScript Map living on the running server process. Specifically:

- Your conversation is held in the session's memory while it's open
- Server reboots, deploys, or session timeouts → conversation is gone
- There's no database table holding chat messages
- "View past conversations" is not a feature today, deliberately

When you close the browser tab, the session continues briefly server-side (so you can reload and recover), then times out and is dropped. No long-term storage.

## OpenAI's side: 30-day operational logs

OpenAI logs API requests for up to 30 days for abuse-prevention. After that, those logs are deleted per their policy. We don't have access to OpenAI's logs of your queries — that data is solely between you and OpenAI under their terms.

## Will Server Manager ever use a different LLM provider?

Possibly. The architecture supports swapping providers (Anthropic Claude is wired but not the production default). If that changes, this page will be updated, and any material privacy difference will trigger a re-acceptance of the Terms of Service.

## Summary

| What | Sent to OpenAI? | Stored by Server Manager? |
|---|---|---|
| Your chat messages | Yes | RAM only (session lifetime) |
| System prompt + tool definitions | Yes | Static in our code |
| Server inventory output | Yes (when relevant) | RAM only |
| Config file contents Faro reads | Yes (when relevant) | RAM only |
| Command outputs from your server | Yes (when relevant) | RAM only |
| SSH credentials | **No** | Encrypted (saved) or RAM-only (active) |
| Your encryption passphrase | **No** | **Never stored anywhere** |
| Card / payment info | **No** | Stripe handles; we store only opaque IDs |
| Email address | **No** | Yes (account record) |

For the legally-binding wording, see the [Privacy Policy](/legal/privacy).

---

# Can I use Server Manager with my existing VPS?

URL: https://servermanager.dev/help/can-i-use-my-existing-vps
Category: Get started
Last updated: 2026-05-26

> Yes. Any Linux server with SSH access works — VPS, dedicated, bare-metal, or home server. No special agent installed; connect with the IP, username, and SSH key (or password); deploy as if it were fresh. Existing nginx, Apache, or Traefik setups are detected and can be migrated to Caddy with a one-click recipe.

Yes — and you don't have to wipe it first. Any Linux server with SSH access works: VPS, dedicated server, bare-metal, or a home server you reach over the network. There's nothing to install on the server side; Server Manager connects over standard SSH and runs commands the way you would manually.

The provider doesn't matter: Hetzner, DigitalOcean, Linode, OVH, Oracle Cloud, AWS Lightsail, Vultr, your local provider, a home server with a public IP — all fine. The connect modal asks for:

- The server's **IP address** (or hostname)
- The **SSH username** (usually `root`, `ubuntu`, or `debian`)
- The **SSH key** (recommended) or password

If you already have things running on the server (a website, a database, etc.), Server Manager detects them — the [[home-screen]] lists every site and service it found as a [[workload]] card. You can manage what's there, add new ones alongside, and leave anything you don't want touched alone.

## Already have nginx, Apache, or Traefik?

Server Manager detects your existing web server and shows it in the [[web-server-tab|Web server tab]] of [[server-info|Server Info]]. Two supported paths:

- **Stay on your current engine.** Faro configures it natively in chat (`/etc/nginx/sites-available/` + `certbot --nginx` for nginx, the Apache equivalent for Apache, Docker labels for Traefik). The recipe palette marks Caddy-writing recipes with a 💬 **via chat** badge — they still work, one extra approval click each.
- **[Migrate to Caddy](/help/migrate-to-caddy)** with a built-in recipe. It rehearses on alt ports `:8080` / `:8443` while your existing engine keeps serving live traffic, Faro verifies each site itself, then atomically swaps. Auto-rollback fires if anything fails. After migration, every Server Manager flow works directly.

Either choice keeps your existing sites running. The migration recipe is opt-in and runs from a button in Server Info → Web server (or automatically as the first step when the Connect Domain wizard detects you'd benefit from it).

---

# Deploy a web app from a git repo

URL: https://servermanager.dev/help/deploy-web-app-from-a-git-repo
Category: Deploy & manage sites and apps
Last updated: 2026-05-23

> Paste your repo URL, type your domain, click Clone & deploy.

> Don't have a git repo? → [Deploy a web app from your computer](/help/deploy-web-app-from-your-computer)

If your web app lives in a public git repo (GitHub, GitLab, Bitbucket, Codeberg, Gitea — any HTTPS git host), Server Manager can clone it directly onto your server. We detect the runtime (Node, Python, or Go) after the clone and install whatever it needs. Updates later are a one-click [Pull latest from git](/help/pull-latest-from-git).

## 1. Open the Actions menu

In the top bar, click **Actions**. In the palette that opens, choose **Deploy from my computer** (under "Bring something in") — or just type "deploy" in the search box.

![Click Actions in the top bar, then choose Deploy from my computer](/help/deploy-web-app-from-a-git-repo/01-set-up-menu.svg)

## 2. Pick "Web app" and switch the Source

A new window opens. Click the **Web app** tab. Then, under **Source**, click **From a git repo**.

![Click Web app, then switch Source to From a git repo](/help/deploy-web-app-from-a-git-repo/02-web-app-source-git.svg)

## 3. Paste your repo URL

Paste the `https://` URL of your repo — for example, `https://github.com/yourname/my-api`. Use the HTTPS form, not the `git@` SSH form.

![Paste your repo URL into the field](/help/deploy-web-app-from-a-git-repo/03-repo-url.svg)

## 4. Type your domain

Type the address you want the app to live at — for example, `api.example.com`. You can leave it blank for now and add a domain later.

![Type your domain in the Domain field](/help/deploy-web-app-from-a-git-repo/04-domain.svg)

## 5. Click "Clone & deploy"

![Click Clone & deploy at the bottom-right](/help/deploy-web-app-from-a-git-repo/05-deploy.svg)

## 6. Done

The window closes and the chat takes over. We clone your repo, install the runtime if needed, start your app under [[systemd]], and point [[caddy|Caddy]] at it as a [[reverse-proxy]]. When it finishes, your app appears on the home screen with a green dot — and a **[Pull latest from git](/help/pull-latest-from-git)** button appears in its service panel.

![Your new web app appears in the overview with a green dot](/help/deploy-web-app-from-a-git-repo/06-done.svg)

## Updating later

Click **[Pull latest from git](/help/pull-latest-from-git)** in the app's service panel — we'll `git pull` your latest commits, install any new dependencies, and restart your app. For private repos, generate a **deploy key** from the same panel; pulls won't ask for credentials again. The full breakdown (conflict handling, deploy keys, monorepo subdir behavior) lives in [the Pull latest article](/help/pull-latest-from-git).

## Want to fine-tune things first?

There are two **Advanced** sections in the window.

### Advanced (git) — branch, subdir, private repos

![The Advanced (git) section expanded — Branch, Subdirectory, Personal access token](/help/deploy-web-app-from-a-git-repo/07-advanced-git.svg)

**Branch.** Override the default branch — anything `git clone --branch` accepts, including tags and commit SHAs.

**Subdirectory.** For monorepos: deploy just one folder inside the repo (e.g., `apps/api`). Sparse-checkout keeps **Pull latest** working.

**Personal access token.** For private repos — used once for the initial clone, then scrubbed. For long-lived pulls, generate a **[deploy key](/help/pull-latest-from-git#for-private-repos-generate-a-deploy-key-first)** after deploy.

### Advanced — runtime, deploy mode, app name, port

The runtime fields work the same way as when deploying from your computer — runtime override, Native vs Container, app name, port, start command, and environment variables. See [Deploy a web app from your computer](/help/deploy-web-app-from-your-computer) for the breakdown, and [Native vs Container](/help/native-vs-container) for the deploy-mode trade-off in depth.

---

# Your GDPR rights & data export

URL: https://servermanager.dev/help/gdpr-rights-and-data-export
Category: Privacy & data safety
Last updated: 2026-05-27

> Under EU GDPR you have the right to access, correct, delete, and export your data, and to object to certain processing. How to exercise each right at Server Manager: mostly via email, with response within 30 days as required by law.

EU data-protection law gives you six concrete rights over the data Server Manager holds about you. Here's what each one means in practice and how to exercise it.

## 1. Right of access (Art. 15)

**What it is:** you can request a copy of all personal data we hold about you.

**How to exercise:** email **support@servermanager.dev** with subject *"GDPR access request"* from the email address registered to your account. We'll send you a machine-readable export (JSON) of:

- Your account record
- Your saved server profiles (alias, host, port, username — *not* the encrypted credential blob, which is useless without your passphrase, and not your passphrase either since we don't have it)
- Your usage logs (per-LLM-call records)
- Your billing records on our side (pass dates, costs, payment method IDs — Stripe holds the actual payment data; export from Stripe separately if needed)
- Per-purchase consent records (timestamps, accepted TOS / Privacy versions)
- Free-tier grant + observed-VPS-host records
- File snapshots (Undo records) — file paths + base64 content

**Response time:** within 30 days. Free of charge, unless the request is "manifestly unfounded or excessive" (it almost never is).

## 2. Right of rectification (Art. 16)

**What it is:** you can ask us to correct inaccurate or out-of-date personal data.

**How to exercise:**

- **Your name** is pulled from your Google or Microsoft account if you used social sign-in. Update it on that provider's side; it'll sync on next login.
- **Your email address** is your unique identifier. To change it: email us — we don't have a self-service email-change flow yet.
- **Your billing address / tax ID** lives at Stripe. Click your avatar → Account → **Manage billing** to update it in their customer portal.

For anything else, email **support@servermanager.dev**.

## 3. Right of erasure ("right to be forgotten") (Art. 17)

**What it is:** you can ask us to delete your data entirely.

**How to exercise:** the simplest path is the **Delete account** section on your Account page (scroll to the bottom). One click + a confirmation step + your email retyped = your account is queued for deletion.

**What happens:**

- Account immediately deactivated (signed out, cannot sign back in)
- Soft-deleted for 30 days in case you change your mind (contact us to restore)
- Hard-deleted after 30 days by an automated job — all your data is removed from the database

**Exceptions** (data we may retain after deletion):

- **Billing records** required by Italian tax law for 10 years (invoice records, transaction amounts, your VAT-ID if you provided one). These are kept by Stripe AND in our own audit-log database in minimized form.
- **Per-purchase consent records** linked to those billing records (the Art. 16(m) waiver audit trail), for the same retention period.

Everything else is fully deleted. See [Deleting your account](/help/deleting-your-account) for the full picture.

## 4. Right to data portability (Art. 20)

**What it is:** you can request your data in a machine-readable, portable format so you can move to another service.

**How to exercise:** same as Right of access — email us, we send JSON. Currently this is a manual export; we'll automate it if demand justifies it.

## 5. Right to object to processing (Art. 21)

**What it is:** you can object to certain processing, particularly for direct marketing or profiling.

**How it applies here:** Server Manager doesn't do direct marketing. We don't profile you for advertising, personalization, or third-party sharing. The only processing we do is:

- Operating the service (running agent commands on your server)
- Billing (Stripe)
- Abuse prevention (free-tier IP collision check, rate limits)
- Service emails (authentication, support replies)

If you object to any of these, the only path is to delete your account — we can't operate the service without doing them.

## 6. Right to restriction of processing (Art. 18)

**What it is:** while we're investigating an access / rectification / objection request, you can ask us to *pause* processing of your data.

**How to exercise:** include "request restriction during review" in your email. We'll suspend your account and pause all processing (except mandatory legal retention) until the underlying request is resolved.

## Contact

**Email:** support@servermanager.dev

Include in your message:

- Your registered email address (must match the sender for us to verify the request)
- Which right you're exercising (access / rectification / erasure / portability / object / restriction)
- Any specifics about what data or what processing

We'll respond within 30 days as required by GDPR. In practice, usually within a few business days.

---

# Will Server Manager break my server?

URL: https://servermanager.dev/help/will-server-manager-break-my-server
Category: Get started
Last updated: 2026-05-24

> Every destructive command requires your explicit approval. Files are auto-backed up to .helm-backup/ before being replaced. SSH credentials never leave your session memory; nothing persists on Server Manager's side after you disconnect.

The short answer: it shouldn't, and we go out of our way to make sure it can't.

**Every destructive command pauses for your approval.** Read-only commands (looking at files, checking service status) run automatically, but anything that writes, deletes, restarts, or installs shows you the exact command and waits for you to click **Yes**. You always see what's about to happen.

**Files that get replaced are backed up.** When the Files tab overwrites or deletes anything, the previous version goes into a `.helm-backup/` folder next to where it lived. Recover it from the Trash panel in the same Files tab — see [Backups: which one do I want](/help/backups) for the walkthrough.

**SSH credentials live only in your session's memory.** They're never written to disk on Server Manager's side. When you sign out or close the tab, the keys are gone from our end.

**Disconnecting Server Manager doesn't touch your server.** The sites and services on it keep running as configured. Server Manager is a *control surface* — it isn't a runtime your apps depend on.

If Faro ever proposes something irreversible (deleting a database, wiping a deploy folder without backup), it warns you explicitly and asks twice.

---

# Pull latest from git

URL: https://servermanager.dev/help/pull-latest-from-git
Category: Deploy & manage sites and apps
Last updated: 2026-05-23

> Click Pull latest in the service panel. We fetch your repo's latest commits and (for web apps) restart your app.

> Haven't deployed yet? → [Deploy a website from a git repo](/help/deploy-website-from-a-git-repo) or [Deploy a web app from a git repo](/help/deploy-web-app-from-a-git-repo)

After a site or app is deployed from a git repo, pushing new commits to your repo doesn't automatically update the server. Click **Pull latest from git** to bring the server up to date — it takes one click, runs `git pull --ff-only` behind the scenes, and (for web apps) installs any new dependencies and restarts your app under [[systemd]].

When the server is behind your repo, the workload's card on the home screen shows a small **`· ⤓ N behind`** pill next to its status — at-a-glance you know there's something to pull. Hover for the exact command.

## 1. Open the service panel

On the home screen, click the card for the site or app you want to update. The service panel opens.

![Click the site or app card on the home screen to open its service panel](/help/pull-latest-from-git/01-open-panel.svg)

## 2. Open the Controls tab and click "Pull latest from git"

For both static sites and web apps, **Pull latest from git** lives in the **Controls** tab, alongside Restart / Stop / Start / Generate deploy key.

![Controls tab for static site and web app — Pull latest from git in both](/help/pull-latest-from-git/03-manual-locations.svg)

There are two more places the button appears, for convenience:

- **Static site** — also in the **Files** tab toolbar (⤓ Pull latest from git), since that's where static-site users spend time.
- **Either kind** — on the **Status** tab, when there are new commits to pull. A **"N commits behind origin"** banner shows up with a Pull latest button next to it. Use this view when you want to see exactly what's pending (commit count, current vs remote SHA) before pulling.

![Status tab shows a "behind by N commits" banner with a Pull latest button](/help/pull-latest-from-git/02-pull-latest-banner.svg)

## 3. Faro does the pull

The chat takes over. For a static site, we run `git pull --ff-only` and [[caddy|Caddy]] keeps serving — no restart needed. For a web app, we also install any new dependencies (`npm ci` / `pip install`) and restart your app.

![Chat shows the pull steps + the new commit SHA](/help/pull-latest-from-git/04-chat-pulling.svg)

## 4. Done

The Git status section updates to **"In sync"** at the new commit.

![Status tab now shows In sync at the new commit](/help/pull-latest-from-git/05-in-sync.svg)

## If the site wasn't deployed from a git repo

Faro checks first and tells you in chat: *"This site wasn't deployed from a git repo, so there's nothing to pull."* To replace the files, re-deploy with [Deploy a website from your computer](/help/deploy-website-from-your-computer) (or the [web app equivalent](/help/deploy-web-app-from-your-computer)) — drop a fresh folder onto the same site and we'll replace what's there.

## If pull fails with "would be overwritten"

We use `git pull --ff-only`, which refuses non-fast-forward merges. If anything modified a tracked file on the server — the Files tab, an SFTP client like FileZilla, an SSH session, even the running app itself — the pull can fail with **"Your local changes would be overwritten by merge"**.

The **Git status** section on the Status tab shows you what's changed:

- `M` = a tracked file was edited on the server. **Conflicts with pull.** Use the repo file (discards your local change): run `sudo git checkout -- <file>` from the workload's path, then pull again.
- `?` = an untracked file (e.g., user-uploaded data in `uploads/`, runtime files your app creates). **Doesn't conflict** — git pull leaves untracked files alone. No action needed.

## For private repos: generate a deploy key first

The first time you Pull latest on a private repo, the cached HTTPS credential won't work for unattended pulls. The fix is a one-time **deploy key**.

1. In the service panel, click **Generate deploy key** (next to Pull latest).
2. Faro generates an SSH keypair on the server and shows you the **public key** in chat.
3. Paste it into your repo's settings → **Deploy keys** (GitHub) or **SSH keys** (GitLab, Bitbucket, Codeberg, Gitea). Leave write-access UNCHECKED — read-only is all we need.
4. Reply `done` in chat — Faro switches the git remote from HTTPS to SSH and tests it.

After that, Pull latest works without asking for credentials again.

![Chat showing the deploy key generation — Faro presents the public key for you to paste into your repo's settings](/help/pull-latest-from-git/06-deploy-key.svg)

## Monorepo / subdir deploys

If you deployed from a subdirectory of a monorepo (e.g., `apps/web`), Pull latest still works — sparse-checkout handles it behind the scenes. We pull into the long-lived clone at `/var/www/.helm-repos/<domain>/` (or `/opt/.helm-repos/<app>/` for a native web app) and your deploy directory is a symlink that automatically reflects the new files.

## Switching branches

Pull latest pulls from whatever branch you cloned during the initial deploy. To switch to a different branch, re-deploy from the modal — pick the new branch in **Advanced (git)**.

---

# Deleting your account

URL: https://servermanager.dev/help/deleting-your-account
Category: Privacy & data safety
Last updated: 2026-05-27

> One-click deletion on the Account page. Account is immediately deactivated; data is soft-deleted for 30 days in case you change your mind; hard-deleted after that. Your servers on your hosting provider are untouched — Server Manager only releases its record of them.

Whenever you want to stop using Server Manager, here's how to remove your account and what happens to your data.

## How to delete

1. Click your avatar in the top-right → **Account**
2. Scroll to the bottom — **Delete account** is the last section
3. Read the confirmation text, type the requested confirmation, click the danger-colored button
4. You'll be signed out immediately. Any open sessions on other devices stop working too.

That's it. No support ticket required.

## What happens immediately

- **Account deactivated.** You can't sign in. The email is "owned" by the soft-deleted account for 30 days (you can't re-sign-up with the same email during this window).
- **Active SSH sessions ended.** All in-memory state is dropped.
- **Saved server profiles inaccessible.** Their database rows still exist (soft-deleted state) but no code path can read them anymore.
- **Auto-refill stopped.** No future charges will happen.

## The 30-day grace period

For 30 days after you click delete, your account is in **soft-deleted** state. During this window:

- All data is preserved in the database but flagged as deleted
- You can't access anything yourself
- If you change your mind, **email support@servermanager.dev from the same address** and we'll restore your account — minus any work that ran on your server in the meantime

The 30-day grace is a safety net against accidental deletion. It's not a normal-use feature; if you actively want the data gone faster, mention that in your delete confirmation and we'll honor the request.

## After 30 days — hard delete

A nightly job removes every soft-deleted account that has crossed the 30-day mark. What this means in concrete terms:

**Deleted from our database:**

- Your user record (email, name, account dates, TOS acceptance)
- All saved server profiles (alias, host, port, encrypted credential blobs)
- All session and authentication tokens
- VPS claim records (host + timestamps)
- Free-tier grant + observed VPS hosts
- Per-LLM-call usage logs
- File snapshots from Undo
- Stripe customer ID and payment method ID on our side

**Retained for legal compliance** (Italian tax law requires 10 years):

- Billing audit records: pass purchase dates, amounts, invoice IDs. These are minimized — only what's required to file taxes correctly.
- Purchase consent records (the Art. 16(m) waiver audit trail) linked to the billing records.

**Held by third parties, not us:**

- **Stripe** retains your payment records per their own retention policies (typically 7+ years for tax / fraud / dispute reasons). You can delete your Stripe customer record directly via Stripe's own GDPR process if needed.
- **OpenAI** held your chat content briefly in their 30-day operational log (which they delete automatically). Deleting your Server Manager account doesn't reach into OpenAI's logs — they're managed under OpenAI's own retention policy.

## What does NOT happen

- **Your servers on your hosting provider stay running.** Server Manager only manages your servers; it doesn't own them. Hetzner, DigitalOcean, AWS, your home Raspberry Pi — they keep doing what they were doing. Server Manager only releases its record of them; nothing on the server side is touched.
- **Sites and services you deployed stay deployed.** All the WordPress installs, Docker containers, Caddy configs, database services that Faro set up are still running on your server. Server Manager is a remote-control product, not a hosting provider.
- **Any DNS or email-routing changes made by Server Manager stay in place.** A records pointing at your server, Cloudflare email-routing rules — those live in your DNS provider's account, not ours. You'd manage them there.

If you want to clean up the actual server too, do that *before* you delete your Server Manager account (so you still have the tools to log in via Faro and remove things). Or do it manually via SSH afterwards.

## What if I delete and want to come back?

- **Within 30 days**: email support@servermanager.dev from the same address — we'll un-soft-delete the account.
- **After 30 days**: sign up fresh. Note that the free-tier anti-abuse system will recognize previously-connected server hosts and may revoke the new account's free trial if it matches (see [How we prevent abuse](/help/what-is-server-manager)). You can buy a paid pass normally.

## Edge cases

**I can't sign in to my account but want to delete it.** Email us with proof of ownership (the email you registered with, a few details about your usage). We'll delete it manually.

**I want to delete just *some* of my data**, not the whole account. That's the [right of erasure for specific data](/help/gdpr-rights-and-data-export) — email us with what specifically you want removed.

**I'm an admin (multi-user accounts)**. Server Manager doesn't have multi-user accounts yet; each account is a single user. So this case doesn't apply today.

---

# What is Faro?

URL: https://servermanager.dev/help/what-is-faro
Category: Get started
Last updated: 2026-05-24

> The AI agent inside Server Manager. Faro reads your chat messages, proposes commands to run on your server, and explains what each one does. You approve; Faro runs them.

**Faro** is the AI agent inside Server Manager. It's what you're talking to when you type in the chat panel.

Faro's job is to translate your plain-English requests into commands that run on your server over SSH. It:

- **Reads** what you want ("install WordPress on this server", "why is my site down?")
- **Proposes** the commands needed and explains what each does
- **Pauses for approval** on anything that changes the system
- **Runs** the approved commands and shows you the output as it streams
- **Adapts** to what it sees — if a command fails, Faro reads the error and proposes the next step

Faro never runs anything destructive without your explicit "yes". Read-only checks (looking at files, listing running services) execute automatically because they can't break anything.

**Naming**: Server Manager is the *product* you're using. Faro is the *agent* inside it. You can think of Faro as your senior sysadmin colleague — but one that types fast, never forgets a command, and pauses to check with you before doing anything important.

---

# Multiple sites on the same server

URL: https://servermanager.dev/help/multiple-sites-on-the-same-server
Category: Deploy & manage sites and apps
Last updated: 2026-05-23

> Each site or app on a Server Manager–managed server lives in its own folder, keyed off its domain (or app name). Different identifiers coexist; reusing the same identifier replaces what's there.

A single server can host as many websites and web apps as it has RAM and disk for. The rule for whether a new deploy lives **alongside** an existing one or **replaces** it is the same in every flow: it depends on the **identifier** you give it.

## The rule

The identifier is what locks one deploy to one slot on the server:

- **Static site** → identifier is the **domain** you typed during deploy.
- **Web app** → identifier is the **app name** in the Advanced section (defaults to your folder name, e.g. `my-api`).
- **Database** → identifier is the **DB name** + the engine (each engine — Postgres / MySQL / MariaDB — runs in its own container).

| What you do | What happens |
|---|---|
| Deploy a static site with a **new domain** | New `/var/www/<new-domain>/` folder, new [[caddyfile|Caddyfile]] block, lives alongside everything else. |
| Deploy a static site with an **existing domain** | The existing `/var/www/<domain>/` directory is wiped and replaced with your new files. Faro asks for approval first. |
| Deploy a web app with a **new app name** | New `/opt/<new-app>/` folder, new [[systemd]] unit `<new-app>.service`, new Caddy reverse-proxy block for its domain. |
| Deploy a web app with an **existing app name** | The existing `/opt/<app>/` is wiped and replaced. The service is restarted with the new code. |

## Static sites — keyed by domain

Each static-site deploy lives at `/var/www/<domain>/` and gets its own [[caddyfile|Caddyfile]] block:

```
mysite.example.com { root * /var/www/mysite.example.com; file_server }
api-docs.example.com { root * /var/www/api-docs.example.com; file_server }
```

Two different domains = two different folders, two Caddyfile blocks, both serving in parallel. [[caddy|Caddy]] handles the HTTPS certificates for each independently.

If you redeploy to the **same** domain, Faro narrates the about-to-replace step in chat:

> *"Heads up: `/var/www/mysite.example.com/` already contains a previous deploy. Current contents: `index.html, about.html, assets/`. Everything in there gets replaced with your N uploaded files. If you have a runtime dir (uploads/, data/, etc.) you want to keep, cancel now, add an empty `.helm-keep` file inside it via the Files tab, and re-run the deploy."*

The `.helm-keep` marker is the escape hatch — see [Preserve a folder across redeploys](#preserve-a-folder-across-redeploys) below.

![Two static sites on the same server, each in its own /var/www/ folder, with their own Caddyfile blocks](/help/multiple-sites-on-the-same-server/01-two-static-sites.svg)

## Web apps — keyed by app name

Each web-app deploy lives at `/opt/<appName>/` and runs as its own [[systemd]] unit:

```
/opt/my-api/      → my-api.service      (Node, port 3000)
/opt/scraper/     → scraper.service     (Python, port 5000)
/opt/notifier/    → notifier.service    (Go, port 8080)
```

Three different app names = three independent services. Each one has its own internal port, its own user, its own logs. [[caddy|Caddy]] reverse-proxies a domain to each. Resource cost: small (Node and Python are ~50–200 MB RSS depending on app).

If you redeploy with the **same** app name, the existing `/opt/<app>/` is replaced and the service restarts with the new code. Same `.helm-keep` escape applies.

![Three web apps on the same server, each in its own /opt/ folder with its own systemd unit](/help/multiple-sites-on-the-same-server/02-three-web-apps.svg)

## The "no-domain" slot is single

If you deploy a static site or web app **without a domain**, it goes to a special "default" slot:

- Static: `/var/www/default/`
- Web app: same `/opt/<appName>/` rules apply (you still pick an app name)

For static sites, **there is only one default slot.** A second no-domain static deploy replaces the first because the slot identifier is the literal string `default`. If you want two IP-only static sites, you need to give at least one a real (sub)domain.

For web apps, the app-name identifier still differentiates — IP-only web apps coexist fine as long as they have different app names, because the underlying port is internal and Caddy is what maps `:80` to one of them. (In practice, only one no-domain web app can be the "default" Caddy target on port 80 at a time. Faro figures this out and tells you.)

## Preserve a folder across redeploys

When you redeploy to the same identifier, the default is wipe-and-replace. To keep a specific subdirectory (typical case: user-uploaded data in `uploads/`, runtime caches in `data/`), drop an empty file named **`.helm-keep`** inside it before the next redeploy:

1. Open the site's service panel → **Files** tab.
2. Navigate into the subdirectory you want to keep (e.g., `uploads/`).
3. Click **+ New file** in the toolbar, type `.helm-keep`, press Enter.
4. Redeploy as usual — Faro detects the marker and stashes that subdirectory before wiping, then restores it after the new files land.

If you'd rather do it outside the panel, any of these also work: drag-and-drop an empty `.helm-keep` from your computer into the subfolder, upload it via SFTP (FileZilla, Cyberduck), SSH in and run `sudo touch /var/www/<domain>/<subdir>/.helm-keep`, or just ask Faro in chat (*"create an empty `.helm-keep` in `/var/www/mysite.example.com/uploads/`"*).

Faro narrates this in chat before the destructive step, listing the kept-dirs so you can confirm:

> *"Preserving these because they have a `.helm-keep` marker inside: `uploads/, data/`. Everything else gets replaced with your N uploaded files."*

If the new upload also contained a `uploads/` directory, the **preserved server-side version wins** — Faro tells you afterwards that your upload's `uploads/` was dropped. To force the upload's version to win instead, remove the `.helm-keep` marker and redeploy.

![A static site with .helm-keep markers inside uploads/ and data/, preserved across a redeploy](/help/multiple-sites-on-the-same-server/03-helm-keep.svg)

## Resource limits

There's no hard cap on how many sites you can run — it's bounded by RAM and disk. Rough rules of thumb on a small server (2 GB RAM):

- **Static sites are nearly free** — files on disk + a Caddy block. You can easily run 50+ of them. The constraint is disk, and each one is usually tiny (≤ 100 MB).
- **Native web apps** cost ~50–200 MB RSS each, depending on language and what they do. A 2 GB box comfortably runs 5–8 of them alongside Caddy + a database.
- **Container web apps** add ~30–100 MB per container on top of the runtime cost — pin runtime versions but cost more RAM. See [Native vs Container](#) for the trade-off.
- **Databases** are the heaviest — Postgres or MySQL idle takes ~100–300 MB; under load, the buffer pool can grow to a configurable cap.

The Status tab on each service panel shows current memory + CPU; the Server panel will (in a future release) show the total roll-up across everything on the box.

---

# Watch your servers from your browser

URL: https://servermanager.dev/help/watch-servers-from-your-browser
Category: Get started
Last updated: 2026-06-08

> A free browser extension that shows each monitored server's health — load, memory, disk, pending updates — right in your toolbar, even when the app is closed. It alerts you when a site goes down, and one click jumps you into Server Manager to fix it.

The Server Manager browser extension puts your servers' health right in your browser toolbar — **even when the app is closed**. At a glance you can see whether each server is healthy, whether updates are waiting, and whether a site has gone down — and one click takes you straight into Server Manager to fix it.

It's **free**, **read-only**, and entirely optional. You don't need it to use Server Manager — it's just a faster way to keep an eye on things between visits.

![The extension popup showing a server card with load, memory, disk, pending updates, and an Open dashboard link](/help/watch-servers-from-your-browser/01-popup-healthy.svg)

## What it shows

For each server you've turned monitoring on, the popup shows a card with:

- **Load (1-minute average)**, **Memory**, and **Disk** — the key "is this server under strain?" numbers, with a small bar that turns amber/red as things fill up.
- **"updated Xs ago"** — how fresh the reading is (the server reports in every ~5 minutes).
- **Pending updates** — e.g. *"3 updates available · 2 security"*.
- **A "Site not responding" alert** — if a site that was serving stops responding.
- A **Fix in Server Manager →** button on anything that needs action, which opens the app already pointed at the right server.

There's also a small **badge on the toolbar icon**: it shows how many of your servers need attention, coloured by severity — **red** when something's down or critical, **amber** for pending updates, nothing when all's well. So you get a nudge without even opening the popup.

## How it works (the short version)

When you turn on monitoring for a server, Server Manager installs a small **read-only agent** on it (a script that runs on a schedule as a locked-down, no-login system user). Every ~5 minutes it reads the server's own metrics and update counts and sends them to your account over HTTPS. The extension reads that status and draws the popup.

The agent **cannot change anything**, run commands, or read your files, and it **stores no SSH keys**. See [How your SSH credentials are handled](/help/how-ssh-credentials-are-handled) for the security model.

## Step 1 — Install the extension

Install it from the [Chrome Web Store](https://chromewebstore.google.com/detail/server-manager/gcopnlmdcfoffapmhkpldjnggeggbcbg). It works in Chrome and Chromium-based browsers (Edge, Brave, and similar).

After installing, you'll see the Server Manager **S** icon in your toolbar. (If it's hidden, click the puzzle-piece "Extensions" button and pin it.)

## Step 2 — Connect it to your account

The extension needs to be linked to your Server Manager account once:

1. Click the **S** icon, then click **Connect**.
2. A new tab opens on servermanager.dev showing a short code. If you're not already signed in, sign in.
3. Confirm the code matches, then **Approve**.

That's it — the popup now follows your account. You can link more than one browser or device; each one connects the same way.

## Step 3 — Turn on monitoring for a server

The extension only shows servers you've explicitly turned monitoring on for.

1. In Server Manager, connect to the server, open **Server details**, and go to the **Updates** tab.
2. Find the **Watch this server from your browser** card and click **Enable monitoring**.
3. Confirm — Server Manager installs the read-only agent over your existing connection (a few seconds). Nothing else on your server changes.

Repeat for each server you want to watch. The server appears in the extension popup within a few minutes — or open the popup and click **↻** to refresh it right away.

*In Server Manager: Server details → Updates tab → Watch this server from your browser → Enable monitoring.*

## Notifications

If you allow notifications, the extension sends a desktop alert the moment something **changes** — for example a site stops responding or new **security** updates appear. It only fires on a change of state, never repeatedly for the same thing, so it won't nag you. Clicking the notification opens Server Manager pointed at that server.

![Extension popup showing a "Site not responding" alert in red with a Fix in Server Manager button](/help/watch-servers-from-your-browser/02-popup-alert.svg)

You can turn notifications off for the extension at any time in your browser's settings.

## Managing monitoring

- **Update the agent.** When a newer agent is available, the Updates card shows **Monitoring agent update available → Update agent**. One click re-installs the latest version over your connection — same permissions, just newer metrics and checks.
- **Turn monitoring off** for a server: open its **Updates** tab and click **Turn off monitoring**. Server Manager removes the agent from that server. (Reversible — just enable it again later.)
- **Disconnect a browser:** go to **Account → Connected extensions** and click **Revoke** next to the device. That browser stops seeing your servers immediately; your servers aren't affected.

## Is it safe?

Yes — this is the most-asked question, so to be explicit:

| | |
|---|---|
| ✅ Reads each server's **status, metrics, and pending updates** | ❌ **Can't change anything** or run commands |
| ✅ Sends that status to **your own account** only | ❌ **Stores no SSH keys** — your credentials stay encrypted, as always |
| ✅ Talks **only** to servermanager.dev | ❌ No tracking, no ads, not shared with anyone |

It's covered in our [Privacy Policy](https://servermanager.dev/legal/privacy) (the "Browser extension & server monitoring" section), and the monitoring is **free** — it uses none of your usage allowance, because the agent runs on your server, not ours.

## Troubleshooting

**The server shows "No recent status yet" / looks stale.** If you just turned on monitoring, give it about **5 minutes** and reopen the popup — the agent reports in on that schedule. If it keeps showing after that, the server may genuinely be offline or unreachable.

**The badge looks out of date.** The popup refreshes its numbers each time you open it; reopen it to get the latest. The toolbar badge refreshes on its own roughly every 5 minutes.

**I don't see a server I enabled.** Make sure the extension is connected to the **same account** you enabled monitoring under (check **Account → Connected extensions**), and that you clicked **Enable monitoring** on that server's Updates tab.

## Common questions

**Does it cost anything?** No. The extension and the monitoring are free.

**Which browsers does it work in?** Chrome and Chromium-based browsers (Edge, Brave, etc.).

**Will it slow down my server?** No meaningfully — the agent is a tiny read-only script that runs briefly every ~5 minutes and sends a small status update.

**Can the extension or the agent change my server?** No. Both are read-only. Anything that *changes* your server still happens only inside Server Manager, through Faro, with you approving each step.

**Can I watch several servers, or use several browsers?** Yes — enable monitoring per server, and connect the extension on each browser/device you use.

**How do I stop everything?** Turn off monitoring per server (Updates tab → Turn off monitoring) to remove the agents, and/or revoke the browser under Account → Connected extensions.

**Something looks wrong and I'm not sure what to do.** Open Server Manager and ask Faro in the chat — describe what you see (or paste a screenshot) and it'll help you sort it out.

---

# Native vs Container — which should I pick?

URL: https://servermanager.dev/help/native-vs-container
Category: Deploy & manage sites and apps
Last updated: 2026-05-24

> Native runs your web app directly on the host under systemd — fast, low RAM, shared runtime. Container runs it inside Docker — own runtime version per app, ~30–100 MB extra RAM.

> Only relevant if you're deploying a **web app** (Node, Python, Go). Static sites and WordPress don't show this choice.

When you deploy a web app, the **Advanced** section in the deploy window has a toggle: **Deploy as Native process** or **Container**. Both put your app online behind [[caddy|Caddy]] with [[lets-encrypt|Let's Encrypt]] HTTPS — the difference is *how* your code runs underneath.

## What you're choosing

![The "Deploy as" toggle inside the Deploy window's Advanced section](/help/native-vs-container/01-deploy-as-toggle.svg)

- **Native process** — Server Manager installs the runtime once on the host (Node 22 LTS, Python 3 + venv, or Go), then runs your app directly under [[systemd]] as its own service.
- **Container** — Server Manager builds a Docker image for your app with the runtime version you pick, then runs it as a container fronted by host Caddy.

Either way: same domain, same HTTPS, same Pull-latest-from-git button, same Files-tab access to your code.

## How Native works

Your app lives at `/opt/<appName>/` and runs as a [[systemd]] unit called `<appName>.service`. The runtime (Node, Python, Go) is installed once at the OS level and **shared** across every native web app on the server.

![Native deploy — three apps under systemd, all sharing one Node and one Python install on the host](/help/native-vs-container/02-native-architecture.svg)

- **Files** at `/opt/<appName>/`
- **Service** managed by systemd — `sudo systemctl status <appName>` works as expected
- **Logs** captured by journald, surfaced in the Logs tab
- **Runtime** one Node 22, one Python 3, one Go install on the host — all native apps share them

## How Container works

Your app runs inside its own Docker container. The compose file lives at `/opt/<appName>/docker-compose.yml`, and the container is fronted by host Caddy that reverse-proxies your domain to the container's internal port.

![Container deploy — three apps, each in its own Docker container with its own pinned runtime version](/help/native-vs-container/03-container-architecture.svg)

- **Files** at `/opt/<appName>/` — your source code, plus a `Dockerfile` and `docker-compose.yml` we generate
- **Service** managed by Docker — `sudo docker compose -f /opt/<appName>/docker-compose.yml ps` shows status
- **Logs** captured by Docker, surfaced in the Logs tab
- **Runtime** each app pins its own version — `Node 18`, `Node 22`, `Python 3.11`, `Python 3.13` can all coexist

## Which should I pick?

Most apps should start with **Native**. Pick Container only if you have a specific reason.

| If… | Pick |
|---|---|
| You have **one app** on the server and don't care about version pinning | **Native** |
| You want the **lowest RAM overhead** (matters on 1 GB / 2 GB servers) | **Native** |
| You want **fastest deploys** (no image build) | **Native** |
| You have **two apps with different runtime versions** (e.g., Node 18 + Node 22) | **Container** for at least the odd-one-out |
| You want **strict isolation** between apps (e.g., one is third-party code you don't fully trust) | **Container** |
| Your app brings a **complex non-runtime dependency** (specific Postgres client version, ImageMagick build, etc.) | **Container** (everything ships in the image) |
| You plan to **move this app to another host later** | **Container** (the image is portable; native deploys depend on the host's runtime) |

The trade-off in numbers (rough):

- **Native**: ~50–200 MB RSS per app (just the app + interpreter). First deploy: 10–30 s.
- **Container**: ~50–200 MB RSS for the app + ~30–100 MB Docker overhead per container. First deploy: 1–3 min (image build).

On a 2 GB server that's mostly idle, you can run 5–8 native web apps + Caddy + a small database. Containers cut that to ~4–6.

## Switching later

The mode is **intrinsic to the app**: there's no toggle in the service panel to switch a Native app to a Container or vice versa.

To switch, **redeploy** the app from the modal and pick the other option:

1. Open **Actions** → **Deploy from my computer** (or **Deploy a web app from a git repo**)
2. Drop your code or paste the repo URL
3. **Expand Advanced** → flip the **Deploy as** toggle
4. Click **Deploy** — the existing app gets replaced with the new mode

Code stays on disk (the `.helm-keep` rules from [Multiple sites on the same server](/help/multiple-sites-on-the-same-server) apply equally to Container redeploys). Your domain stays pointed at the same place. The downtime is whatever a normal redeploy takes — usually under a minute.

---

# Why does Server Manager use Caddy?

URL: https://servermanager.dev/help/why-caddy
Category: Domains, HTTPS & email
Last updated: 2026-05-26

> Server Manager picks Caddy as the [[reverse-proxy]] because automatic HTTPS works in zero config — type a domain, get a valid cert 30 seconds later. nginx and Apache need certbot + a renewal cron + manual reloads to get there. Caddy collapses all of that into "type domain, get HTTPS." This article compares Caddy, nginx, Apache, and Traefik feature-by-feature, explains the rationale, and tells you when keeping your current engine is the right call.

Server Manager is opinionated: every recipe and wizard writes [[caddyfile|Caddyfile]] blocks, not nginx `server { }` or Apache `<VirtualHost>` or Traefik Docker labels. **That's not because the other engines are bad** — nginx in particular is a fine piece of software. It's because [[caddy|Caddy]] collapses the most painful part of running a website (HTTPS) into zero config, and the others don't. If you already use nginx, Apache, or Traefik, Server Manager detects your setup and you choose: stay on it (with a small UX trade-off) or run the one-click [Migrate to Caddy](/help/migrate-to-caddy) recipe.

## What does a reverse proxy do, again?

The [[reverse-proxy]] is the public-facing server on ports 80 (HTTP) and 443 (HTTPS). Browsers hit it; it forwards each request to the right internal app behind it — your WordPress, your web app, your API. Your apps listen on internal ports like `3000` or `127.0.0.1:8080`; the reverse proxy is the front door that decides what gets routed where.

You need a reverse proxy to:

- Serve **HTTPS** (the padlock icon). Browsers refuse to send cookies / passwords / payments over plain HTTP these days.
- Run **multiple sites on one server** at different domains.
- Hide internal ports — visitors hit `https://mysite.com`, not `https://mysite.com:3000`.

## The four common choices

|                                       | **Caddy**             | **nginx**          | **Apache**           | **Traefik**          |
| ------------------------------------- | --------------------- | ------------------ | -------------------- | -------------------- |
| Automatic HTTPS (zero config)         | ✅ Built-in           | ❌ Needs certbot   | ❌ Needs certbot     | ✅ Built-in          |
| Certificate auto-renewal              | ✅ Built-in           | certbot.timer cron | certbot.timer cron   | ✅ Built-in          |
| One-line reverse proxy                | ✅ `reverse_proxy`    | ❌ multi-line      | ❌ multi-line        | ✅ Docker labels     |
| Sensible defaults (gzip, HTTP/2, TLS) | ✅ On by default      | ❌ Off by default  | ❌ Off by default    | ✅ On by default     |
| Live config reload                    | ✅ Yes                | ✅ Yes (SIGHUP)    | ✅ Yes (graceful)    | ✅ Yes               |
| Single static binary                  | ✅ Yes                | ❌ Multi-file      | ❌ Modules + libs    | ✅ Yes               |
| Config file shape                     | Caddyfile (terse)     | `server { }`       | `<VirtualHost>` + .htaccess | YAML / TOML / labels |
| Best for                              | "I want HTTPS to just work" | High-traffic + complex routing | Legacy PHP / `.htaccess` | Docker-centric stacks |
| Learning curve                        | Low                   | Medium-high        | Medium-high          | Medium               |
| Performance at extreme scale          | Good                  | ✅ Best            | Good                 | Good                 |

The same idea in one line each:

- **Caddy** — "I want HTTPS to just work."
- **nginx** — "I need maximum throughput and have time to configure it carefully."
- **Apache** — "I run enterprise PHP apps with `.htaccess`."
- **Traefik** — "Everything is in Docker, label it."

## Why Server Manager picked Caddy

Three reasons, in priority order:

1. **Automatic HTTPS is the dominant feature for non-technical users.** The single biggest source of friction in deploying a small website is dealing with TLS certificates. Caddy eliminates that friction entirely. The wizard asks you to type a domain; 30 seconds later the site is live on HTTPS with a valid Let's Encrypt cert. No certbot install, no plugin choice, no renewal cron, no config edit. This is the unambiguous win and we're not willing to compromise on it.
2. **Sensible defaults reduce broken-site bugs.** Caddy ships with gzip compression, HTTP/2, modern TLS ciphers, and sane timeouts all on by default. nginx and Apache leave most of these off — which means a real fraction of every nginx deployment ends up with subtly suboptimal config that the user never knew to fix. Caddy makes the right thing the default.
3. **Caddyfile syntax is approachable.** Faro can write Caddyfile blocks reliably; you can read them. The equivalent in nginx requires multi-line `server { listen 80; listen 443 ssl; ssl_certificate /etc/letsencrypt/live/...; ... }` blocks that are harder to scan and harder for an LLM to author accurately. Lower-friction syntax = fewer agent errors + cleaner approval reviews.

## What you give up by choosing Caddy

We try to be honest about trade-offs. Caddy is not strictly better at everything.

- **Maximum performance under millions of requests.** nginx still has the throughput edge. For most websites this doesn't matter — the bottleneck is your backend, not the proxy — but if you're serving hundreds of millions of requests per day, nginx will use a bit less CPU.
- **Niche nginx modules.** `mod_lua` / OpenResty, `proxy_cache_path`, `limit_req_zone`, `geoip` — these are real features Caddy either does differently or doesn't have. If you rely on them, migration isn't a one-click thing.
- **Apache + `mod_php` legacy.** Some old PHP apps assume Apache with `mod_php` (PHP runs inside the Apache process). Caddy uses PHP-FPM (separate process); same functional result, but if you have years of Apache-specific config (custom `.htaccess`, mod_rewrite chains), porting takes work.
- **Existing team familiarity.** If your team already knows nginx inside out, there's a learning cost to switching. Worth it for the auto-HTTPS, but not free.

For Server Manager's target audience (small teams, hobby projects, single-server deployments), none of these usually bite. The auto-HTTPS win is what matters day-to-day.

## What if I keep my current engine?

You can. Server Manager detects nginx, Apache, and Traefik and gives you two supported paths:

- **Stay on your current engine.** Faro configures it natively in chat: `/etc/nginx/sites-available/` + `certbot --nginx` for nginx, the Apache equivalent for Apache, Docker labels for Traefik. Same end result for each domain; the recipe palette marks Caddy-only flows with a 💬 **via chat** badge — they still work, just one extra approval click compared to the Caddy happy path.
- **[Migrate to Caddy](/help/migrate-to-caddy)** with the built-in recipe. Translates your existing config to a Caddyfile, rehearses on alternate ports while your current engine keeps serving live traffic, atomically swaps on success, auto-rolls-back on any failure. ~10 minutes; supports nginx, Apache, and Traefik.

Whichever you pick, your existing sites keep serving traffic. Migration is opt-in.

## When NOT to migrate

Don't migrate to Caddy if any of these apply:

- You use **OpenResty / nginx-lua** or other custom-module-heavy nginx features.
- You have **complex Apache `.htaccess`** rules with stacked `RewriteRule [L,PT,NE]` chains.
- You run **Traefik with custom middlewares** (basic auth, rate limit, header rewrite chains) — translatable in theory but the v1 recipe doesn't auto-do it.
- You're a **shared host** with users you don't trust — you might rely on Apache's `mod_php` per-user isolation models that Caddy doesn't replicate.

The migration recipe's manageability check refuses to run when it detects these. **But that's only about the auto-migration recipe** — every other Server Manager action (deploy, install WordPress, connect a domain, manage backups, debug a slow site, restart a service) still works via the chat path on your existing engine. Faro adapts to whichever proxy is on the server. Try things; if a UI button is gated, Faro will offer to do the same work via shell commands instead.

## Bottom line

Caddy isn't superior in every dimension. It's superior in the **one dimension that matters most for the kind of server most Server Manager users run**: automatic, reliable HTTPS in zero config. Everything else (sensible defaults, single binary, terse syntax, live reload) supports that primary win.

If you already run nginx / Apache / Traefik and your current setup is healthy, the chat path is fully supported. If you'd like the full one-click Server Manager experience, the [Migrate to Caddy](/help/migrate-to-caddy) recipe is one button away in the [[web-server-tab|Web server tab]] of [[server-info|Server Info]].

---

# Backups — which one do I want and how to use it

URL: https://servermanager.dev/help/backups
Category: Backups & recovery
Last updated: 2026-05-24

> Server Manager has four backup-related tools. This article maps each user goal to the right tool, then walks through every flow step-by-step.

Server Manager has **four different backup-related surfaces**, each for a different goal. They overlap conceptually ("save my data so I don't lose it") but pick the wrong one and you'll either do too much work or get a file that doesn't do what you wanted. This article maps each user goal to the right tool, then walks through every flow.

## Quick guide — which one do I want?

| What you're trying to do | Where to go |
|---|---|
| Recover a single file I deleted or overwrote | **Files** tab → **🗑 Backups** |
| Save a portable copy of my whole site/app for safekeeping | **Backup** tab → **Make a backup** |
| Restore a site/app from a bundle I saved earlier | **Backup** tab → **Restore from a bundle** |
| Clone a site/app to a new domain | **Backup** tab → **Restore from a bundle** (same flow, different target) |
| Move a site/app to a different one of my servers | **Backup** tab → **Move to another server** |
| Move *everything* on this server to a new server | Repeat the move flow for each workload — see the note at the bottom of this article |
| Get raw database content (`.sql.gz` you can `psql`/`mysql` into anything) | **SQL Dumps** tab (database workloads only) |
| Preserve a folder across a redeploy (not really a backup) | The `.helm-keep` marker — see [Multiple sites on the same server](/help/multiple-sites-on-the-same-server) |

## Recover a single file (Files tab → 🗑 Backups)

**Use this when**: you deleted or overwrote a file through the Files tab and want it back.

Server Manager auto-saves every file the Files tab is about to overwrite or delete. The previous version goes into a `.helm-backup/` folder next to where it lived. **The last 3 versions** of each name are kept; older ones rotate out.

**Important — it's per-directory.** If you deleted `/var/www/site/blog/post.md`, the backup is at `/var/www/site/blog/.helm-backup/`, not under `/var/www/site/`. To find it, you need to navigate to the directory the file lived in first.

**Steps:**

1. Open the site/app's service panel → **Files** tab.

![Service panel with the Files tab highlighted](/help/backups/01-open-files-tab.svg)

2. **Navigate to the folder** where the file used to live — for example, `/var/www/mysite.example.com/blog/` if you deleted a file in `blog/`.

![Files tab breadcrumb showing navigation into the blog/ folder](/help/backups/02-navigate-folder.svg)

3. Click **🗑 Backups** in the toolbar.

![Files toolbar with the 🗑 Backups button highlighted](/help/backups/03-click-backups-button.svg)

4. Find the entry by name + timestamp. Click **Restore** to put it back, **Download** to save a copy to your computer, or **Delete** to remove the backup permanently from the server.

![Trash panel listing per-file backups with Restore / Download / Delete buttons per entry](/help/backups/04-trash-panel.svg)

Restore is itself reversible — the current state at the target path is snapshotted into `.helm-backup/` before the restore happens, so you can roll back the restore the same way.

**What this DOESN'T cover**: files modified via SFTP/FileZilla, SSH, the running app itself, or a redeploy. Only Files-tab actions trigger the auto-backup. For a redeploy where you want to keep a folder, use [.helm-keep](/help/multiple-sites-on-the-same-server#preserve-a-folder-across-redeploys).

## Back up a whole site or app (Backup tab)

**Use this when**: you want a portable archive of an entire site or app — config, secrets, files, and (for containerized things) the data volumes — that you can keep on your computer for safekeeping or transport.

The Backup tab has three actions, all using the same bundle format. They're the **lifecycle** of a bundle: create → restore later → or transfer to another server.

**What you'll see at the top of the tab.** If there are any bundles for this workload still sitting on the server — typically because you created a backup and didn't download it, or an upload was abandoned mid-restore — they show as a list at the top with size, timestamp, and a **Delete** button per row. This is your cleanup surface: bundles aren't auto-rotated, so old ones quietly eat disk until you remove them. If bundles belong to other workloads, you'll see a small hint counting them; open each workload's own Backup tab to clean those.

### Make a backup

1. Open the service panel → **Backup** tab.
2. **For WordPress, web apps, and databases**: tick **Pause the service during backup** if this is a busy site (active commerce, membership, mid-migration). Default is no-downtime — fast, fine for most cases, but anything written *during* the ~30s capture can be half-captured (typically an orphan file: present in the backup, no DB row). With pause, the service stops briefly (~30–60s downtime) for a perfectly consistent capture. **Static sites don't show this toggle** — there's no managed process to pause ([[caddy|Caddy]] serves the files directly).
3. Click **Make a backup**. The chat takes over — Faro stages the tar command, you approve, the bundle gets built on the server.
4. When ready, a **Download** button appears in chat. Click it; the file streams over SFTP to your computer.

![Backup tab — WordPress version showing the pause toggle, Make a backup / Restore from a bundle / Move to another server as full-width primary buttons in their own sections](/help/backups/05-make-backup.svg)

**What's in the bundle**: the `docker-compose.yml` (or the equivalent service manifest), every secret in `.env`, all named volumes (database data, uploaded files, etc.), and a small `manifest.json` describing the workload. Static-site bundles include the file tree under `/var/www/<domain>/`. The format is self-describing — Restore later reads the manifest and reconstructs everything in the right place.

### Restore from a bundle

**Use this when**: you have a bundle you previously downloaded (or one a teammate sent you) and want to bring back the service — either on this server, or as a clone with a new domain.

1. Open the service panel → **Backup** tab.
2. Click **Restore from a bundle**. An upload window opens (drag-drop or click to choose).
3. Pick the `.tar.gz` bundle. Click **Upload**.
4. After upload, the actual restore happens in the chat. Faro reads the bundle's manifest and either restores in place or — if the bundle's recipe shape doesn't match the current workload, or you point to a different domain — asks whether to clone it to a new domain instead. You review and approve each command before anything runs.

![Restore from a backup — upload window with drag-drop zone, Cancel + Upload buttons at the footer](/help/backups/06-restore-bundle.svg)

**For cloning**: the bundle contains the original domain in its manifest. Faro asks for the new domain and rewrites the Caddyfile + `wp-config.php`-equivalent references so the clone serves at the new address. The original server keeps running untouched.

### Move to another server

**Use this when**: you have a workload on one of your saved servers and want to move (or copy) it to another — without manually downloading the bundle to your computer and re-uploading it on the other side.

This button only appears if you have **at least two saved servers** in Server Manager — the target picker has to have somewhere to point.

**Prerequisite**: make a backup on the source first (the **Make a backup** step above).

1. On the source server's workload, open the service panel → **Backup** tab.
2. Click **Move to another server**. A three-step wizard opens:
   - **Step 1: Pick the target.** Pick a target server from your saved-servers list and enter the target's **encryption passphrase**.
   - **Step 2: Pick the backup.** Choose which bundle to transfer (lists every bundle on the source). Click **Start transfer**.
   - **Step 3: Transfer.** The bundle streams source → target through Server Manager — no copy on your computer, no public S3 in between.
3. When the bundle lands on the target, Server Manager switches you to the target server and offers to restore the just-arrived bundle (one click).

![Move to another server — step 2 of 3, pick the bundle to transfer](/help/backups/07-move-to-server.svg)

## Back up just the database (SQL Dumps tab)

**Use this when**: you only want the database content, not the whole stack. Common cases: handing the data to a developer for testing, importing into a different engine (well, attempting to), or saving a quick safety net before a destructive migration.

This tab only exists for **database workloads** (Postgres, MySQL, MariaDB).

1. Open the database's service panel → **SQL Dumps** tab.
2. Click **Make a dump**. Faro runs `pg_dump` / `mysqldump` (engine-appropriate) and saves the output as `.sql.gz` at `/var/backups/<engine>/`.
3. The new dump appears in the list. **Download** sends it to your computer; **Delete** removes it from the server.

![SQL Dumps tab — list of past dumps with Make / Download / Delete actions](/help/backups/08-sql-dumps.svg)

**SQL Dumps vs Backup tab — same workload, different artifacts**:

- The **SQL Dumps** `.sql.gz` is raw SQL — `psql my-app < dump.sql` restores it into any Postgres of the right major version, including a Postgres running on your laptop.
- The **Backup tab** bundle is a full-stack `.tar.gz` — the database, the compose file, the secrets, the named volumes. Restore recreates the whole container stack.

If you only need to inspect or transplant data: SQL Dumps. If you want to clone the whole database service somewhere: Backup tab.

## Other things often confused with backups

**`.helm-keep` markers** — preserve a folder across a *redeploy* (not a backup, doesn't help with accidental delete). Use when you have a runtime folder like `uploads/` you don't want wiped when you push new code. Covered in [Multiple sites on the same server](/help/multiple-sites-on-the-same-server).

**Moving an entire server.** There's no single "move everything" button — Server Manager moves one workload at a time. To migrate a server with multiple sites/apps, repeat the **Make a backup → Move to another server → Restore on the target** flow for each workload (the **Move to another server** action lives in each workload's **Backup** tab).

## Reference

**Where backups live on disk:**

- Files tab backups → `<original-dir>/.helm-backup/<name>.<timestamp>` (one folder per directory)
- Backup-tab bundles → `/tmp/helm-backups/<id>/<bundle>.tar.gz` on the source server (until you download or delete them)
- Restore-tab uploads → `/tmp/helm-restore/<id>/<bundle>.tar.gz` (until the restore finishes or you delete them)
- SQL Dumps → `/var/backups/<engine>/<dbname>-<timestamp>.sql.gz`

**Retention:**

- Files tab: last **3 versions** per original name, oldest rotates out.
- Backup tab + SQL Dumps: **no auto-rotation** — bundles persist on the server until you delete them from the panel.

**Keeping an eye on disk.** Every workload card on the home screen shows a `· N GB` pill next to its name once it has data. For a deeper breakdown, the Server card's **Server details →** opens Server Info with a **Storage** tab that lists per-workload disk usage (biggest first, with one-click open-panel links), SQL dumps across all engines, and a bulk-cleanup view of every staged bundle on the server. The home screen also surfaces an amber/red alert card when disk crosses 80% / 90%.

**Secrets in bundles:** the `.env` files inside a Backup-tab bundle contain plaintext secrets (database passwords, API keys, etc.). Treat downloaded bundles as you would treat the original `.env` files — don't email them around, don't commit them, don't leave them on shared drives. Bundles are deleted from the source server after download (the file streamed to you was a copy).

---

# Connect a domain to your server

URL: https://servermanager.dev/help/connect-a-domain
Category: Domains, HTTPS & email
Last updated: 2026-07-07

> Point a domain you own at your server and get free HTTPS. The wizard probes where the domain currently lives and walks you through whatever's missing — DNS record, Cloudflare/Porkbun token, Let's Encrypt certificate.

When you deploy a site or app, Server Manager gives it an auto-generated subdomain so you can preview it right away. To run it at your *own* domain (`mysite.com`, `blog.mysite.com`, …), you'll want this wizard.

It does three things, in this order:

1. **Writes the DNS record** at your domain's DNS provider (Cloudflare or Porkbun directly; other providers via a one-time DNS switch to Cloudflare).
2. **Configures your web server** (Caddy) for the new domain.
3. **Gets a free HTTPS certificate** from Let's Encrypt and wires it in.

You don't have to do any of these steps yourself. Server Manager probes where your domain currently lives, figures out what's missing, and lands you on the right step. Close the wizard mid-flow and re-open it later — it picks up where it left off.

## 1. Open the wizard

In the top bar, click **Actions**. In the palette, choose **Point a domain here** (under "Connect your domain & email") — or type "domain" in the search box.

![Click Actions in the top bar, then choose Point a domain here](/help/connect-a-domain/01-open-wizard.svg)

The first time, the **Your domain** field is empty. If you've connected domains before, it pre-fills the most recent one and offers the others in a dropdown.

> **Running more than one site? Where you open this from matters.** There are two entry points. The **Connect a domain** button *on a specific site's card* ties the domain to **that** site. The **Actions → Point a domain here** palette opens the wizard without a site attached. On a server with several sites, the palette route asks you **"What should this domain serve?"** at the final step, so you can pick the right site (or *Create a new static site*) — opening it from the site's card skips that question because it already knows. See [Running several sites on one server](/help/multiple-sites-on-the-same-server).

## 2. Type your domain

Type the address you want — for example, `mysite.com` or `blog.mysite.com`. Hit **Continue** (or press Enter).

![Stage 0 — type your domain and click Continue](/help/connect-a-domain/02-enter-domain.svg)

Server Manager runs a quick probe: looks up the domain's nameservers, looks for an A record, and tries to reach `https://<domain>/`. From the probe results it lands you on whichever stage you actually need.

> **Don't own a domain yet?** Expand the *Don't own a domain yet?* hint on this stage for buying suggestions (Cloudflare Registrar, Porkbun, Namecheap all work). Buy first, then come back.

## 3. (If needed) Switch DNS to Cloudflare

If your domain's nameservers point at a provider Server Manager can't talk to directly — anything that's not Cloudflare or Porkbun — you'll land here. Server Manager will guide you to switch DNS hosting to Cloudflare (free; you keep the domain wherever you bought it).

The wizard shows you the exact six steps:

1. Sign up at [cloudflare.com](https://dash.cloudflare.com/sign-up).
2. From the Account home, find **Domains** → **Add a domain**, enter your domain, pick the Free plan.
3. If Cloudflare shows a *Review your DNS records* page, scroll to the bottom and **Continue to activation** — the imported records are safe to keep.
4. The next page shows you two nameservers like `name1.ns.cloudflare.com`. **Copy both.**
5. Log into wherever you bought the domain and change its nameservers to the two Cloudflare gave you.
6. Wait — propagation usually takes 5–60 minutes. Cloudflare sends you an email when it's done.

![Stage 1 — six-step Cloudflare onboarding guide with a live status block](/help/connect-a-domain/03-switch-to-cloudflare.svg)

The wizard shows a **Last checked** status at the bottom with your current nameservers. After you change them at your registrar, click **Check now** every 5–15 minutes. When the wizard sees Cloudflare nameservers, it moves you to the next stage automatically.

> **Why not just use my registrar's DNS?** The wizard supports any DNS provider with a per-domain API key — currently Cloudflare and Porkbun. Namecheap's DNS API is restricted (per-IP allowlist + a spend gate), GoDaddy's is paywalled, others have similar issues. Cloudflare is the only one that's both free and unrestricted, hence the recommendation. Your domain stays where you bought it — only DNS hosting moves.

> **What if I really want to keep using my registrar's DNS?** You can — just skip the wizard and use the chat instead. Open the chat panel and tell Faro something like *"I want to point yourdomain.com at this server. I'll add the DNS record at my registrar manually."* Faro will tell you the server's public IP, the exact A record to create (`name`, `type=A`, `value=<IP>`, `TTL`), and wait while you paste it into your registrar's dashboard. Once the record propagates, ask Faro to set up Caddy + HTTPS — same end result, just with you doing the click-paste step at the registrar instead of the wizard doing it via API. The wizard exists because that step is fiddly and registrars all do it differently; if you're comfortable doing it yourself, the chat path is fine.

## 4. Paste the DNS API token

This stage is where you authorize Server Manager to write the DNS record for you. The exact steps differ by provider.

### Cloudflare variant

1. Open [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens).
2. Click **Create Token** → use the **Edit zone DNS** template.

::: details Show me — pick the "Edit zone DNS" template
![On the Create API Token page, under "API token templates," click "Use template" next to "Edit zone DNS" (circled in red).](/help/connect-a-domain/cf-token-2-template.jpg)
:::

3. Under **Zone Resources**, restrict to your domain.

::: details Show me — restrict the token to your domain
![In the Zone Resources row, leave "Include" and "Specific zone," then open the third dropdown (circled) and pick your domain — so the token can only touch this one zone.](/help/connect-a-domain/cf-token-3-zone.svg)
:::

4. Click **Create Token**, copy it, paste below.

![Stage 2 (Cloudflare) — paste API token field with the four setup steps above](/help/connect-a-domain/04a-paste-token-cloudflare.svg)

### Porkbun variant

1. Open [porkbun.com/account/api](https://porkbun.com/account/api). Create a new API key — you'll get **two halves**: an `apikey` starting with `pk1_` and a `secretapikey` starting with `sk1_`. Save both.
2. Open [porkbun.com/account/domainsSpeedy](https://porkbun.com/account/domainsSpeedy), find your domain, expand **Details**, and toggle **API Access** ON for that specific domain. (Default is OFF; Porkbun refuses API calls until you enable it per-domain.)
3. Paste both keys below.

![Stage 2 (Porkbun) — two PasswordField inputs for the public + secret halves](/help/connect-a-domain/04b-paste-token-porkbun.svg)

**The token stays in this dialog only.** It's sent over HTTPS to Cloudflare/Porkbun to write the record, and it never gets written to disk on Server Manager's side. If you close the wizard, the token's gone — you'd paste a fresh one next time. You can also revoke it at the provider's dashboard any time.

## 5. Pick the target IP

Server Manager has detected this server's public IP. By default the new DNS record will point at it.

![Stage 3 — preview of "(domain) → (this server's IP)" with a confirm button](/help/connect-a-domain/05-pick-target.svg)

If you typed a subdomain (e.g. `blog.mysite.com`), the wizard auto-fills the subdomain prefix. The preview line shows you exactly what record will be created — for example, `blog.mysite.com → 203.0.113.42`.

> **Point at a different IP?** Expand the *Point at a different IP (not this server)* collapsible if you want the domain to point somewhere else — a different server you haven't connected yet, a load balancer, a CDN. Server Manager writes the DNS record; HTTPS setup has to happen on whichever server the IP belongs to.

> **No port to choose — and you don't need one.** A DNS record maps a *name* to an *IP address*; it can't carry a port. Which of your sites answers for a given domain is decided by [[caddy|Caddy]] using the domain name itself — one Caddyfile block per domain — so any number of sites share ports 80/443 with no conflict. (Ports only matter for reverse-proxied apps, where Caddy fills them in for you.) That's also why the wizard needs to know *which* site the domain is for: it's set by where you opened the wizard, or by the "What should this domain serve?" picker on the last step. See [Running several sites on one server](/help/multiple-sites-on-the-same-server).

If a record with this name already exists at your DNS provider, **its content gets replaced** — Server Manager doesn't keep duplicate A records. That's usually what you want; if it's not, edit it at the provider's dashboard first.

Click **Write the DNS record**. The wizard writes the record and moves on within a couple of seconds.

## 6. Attach to this server (Caddy + HTTPS)

DNS is set — your domain now points at this server. One more step: tell the web server about it and get an HTTPS certificate.

The wizard asks for an **admin email for Let's Encrypt**. It's used only for cert-renewal warnings (rare), and any address you can read works — it doesn't need to be at your new domain.

![Stage 4 — admin email field with confirmation that DNS is set](/help/connect-a-domain/06-attach.svg)

Click **Continue**. The chat takes over: Faro stages the Caddy config update + cert request, you approve each step, the certificate gets issued, and within a few seconds the site is live at `https://yourdomain`.

> **What's Caddy?** The web server running on your server. Server Manager configures it for you — you don't need to touch its config file. If you're curious what changed, the [[caddyfile|Caddyfile]] is at `/etc/caddy/Caddyfile` and gets a new block per connected domain.

> **What's Let's Encrypt?** A free certificate authority. The browser lock icon on `https://` sites comes from a certificate issued by one of those authorities; Let's Encrypt issues them in 30–60 seconds via an automated protocol called ACME. Caddy handles ACME for you in the background.

## 7. Done

You'll see the success screen — *🎉 Your site is live at https://yourdomain* — with an **Open site** button that opens the new URL in a new tab.

![Stage 5 — success card with Open site button](/help/connect-a-domain/07-done.svg)

Your Let's Encrypt certificate **auto-renews every ~60 days**. No further action needed.

## What if the wizard skips ahead?

The probe at stage 0 looks at:

- **`https://<domain>/` responds 2xx/3xx** → goes straight to stage 7 (Done). The site already works; nothing for the wizard to do.
- **A record matches this server's IP** → skips to stage 6 (Attach). DNS is already pointing here; just need Caddy + cert.
- **NS is Cloudflare or Porkbun + token already validated this session** → skips to stage 5 (Pick target).
- **NS is Cloudflare or Porkbun** → stage 4 (Paste token).
- **NS is somewhere else** → stage 3 (Switch to Cloudflare).
- **No domain typed yet** → stage 2 (Enter domain).

That's why re-opening the wizard mid-flow always lands you on the *next* missing step — the probe re-runs and figures out where you are.

## Certificate lifecycle

Let's Encrypt certificates are valid for **90 days** — that's the CA's policy, not configurable. You don't have to do anything to renew them.

**Auto-renewal is on by default.** Caddy runs as a long-living process on your server with a built-in ACME client. When a cert has ~30 days of life remaining (so every ~60 days from your perspective), Caddy quietly issues a fresh one from Let's Encrypt and rotates it in. No cron job, no `certbot.timer`, no maintenance window — and no off switch in our flow.

**How you'd find out if something went wrong.** Two channels:

- **From Let's Encrypt directly** — if a cert is approaching expiry and hasn't been renewed yet, Let's Encrypt sends warning emails to the admin email you provided at the *Attach to this server* step. They arrive at roughly 20 days, 10 days, and 1 day before expiry. Once Caddy renews, those warnings stop. This is the safety net for the rare case Caddy can't renew (e.g., DNS broke, the firewall closed port 80, Let's Encrypt rate-limited you).
- **From the site itself** — if a cert actually expires, the browser shows a *Not secure* / *NET::ERR_CERT_DATE_INVALID* warning. The UI doesn't currently surface cert health proactively, but you can always ask Faro in chat — try *"check the cert for yourdomain.com"* and Faro will run the right `openssl s_client` + `journalctl` commands and tell you who issued the cert, when, and how many days until it expires. If a renewal failed, Faro can also propose the fix bundle on the spot.

**How to check the cert by hand:** on the server, `sudo journalctl -u caddy | grep -i certificate` shows every issuance + renewal Caddy has done. The cert + private key live under `/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<your-domain>/`. You can also ask Faro in chat — *"check the cert for yourdomain.com"* — and it'll run the right `openssl s_client` / `journalctl` commands and tell you what it sees.

## What if my setup is different?

### I want a different TLS certificate (not Let's Encrypt)

The wizard doesn't surface this, but Caddy supports it. Common reasons: you bought a paid cert from DigiCert / Sectigo, your organization issues its own, or you want a Cloudflare Origin Certificate (15-year cert that only works with CF as a reverse proxy).

**Use the chat path:** tell Faro *"I want to use a custom TLS cert for yourdomain.com — here's the cert and key"* and either paste the PEM content or upload the two files (Files tab). Faro will place them under `/etc/caddy/certs/yourdomain.com/`, edit the site's Caddyfile block to add `tls /etc/caddy/certs/yourdomain.com/fullchain.pem /etc/caddy/certs/yourdomain.com/privkey.pem`, and reload Caddy. From then on Caddy serves your cert instead of asking Let's Encrypt.

**You're responsible for renewals** with a custom cert — Caddy won't auto-renew something it didn't issue. Set yourself a calendar reminder for the cert's expiry date, get a fresh one from your CA, swap it in the same path, reload Caddy. Faro can do the swap on demand.

### I'm not using Caddy — I have nginx / Apache / Traefik

Server Manager detects your existing engine when you connect, and gives you **two supported paths**. Both work end-to-end. We picked [[caddy|Caddy]] as the default because it makes HTTPS one-click — read [Why does Server Manager use Caddy?](/help/why-caddy) for the rationale and a comparison table. If you'd rather keep your current engine, the chat-path option below has you covered with no UX loss for one-off domain setup.

**On a non-Caddy server, opening the [[connect-domain-wizard|wizard]] lands you on a small routing screen first** with three buttons. Pick one and follow the matching walkthrough below.

#### Option A — Migrate to Caddy, then run the wizard

The cleanest long-term path. After migration, every Server Manager flow works directly (this Connect Domain wizard included), and Caddy's automatic HTTPS replaces certbot + cron with zero ongoing maintenance.

1. On the routing screen, click **Migrate this server to Caddy**. (Equivalent button at any time: [[server-info|Server Info]] → [[web-server-tab|Web server]] → **Migrate this server to Caddy →**.)
2. The chat panel opens with Faro starting Phase 0 (read-only pre-flight). It enumerates your vhosts, checks for unsupported directives, finds your Let's Encrypt admin email, and asks you to reply `go`. **Click the `Reply: go` quick-reply button.**
3. Faro then walks 4 more phases, each pausing for your approval before any destructive command: Phase 1 installs Caddy (your existing engine still owns the live ports); Phase 2 writes the translated Caddyfile to `/tmp` and validates; Phase 3 rehearses Caddy on alternate ports `:8080` / `:8443` and verifies each site itself with loopback curls; Phase 4 atomic-swaps and auto-rolls-back if any site fails verification.
4. After Faro reports `Migration complete`, **re-open the Connect Domain wizard.** It now runs the normal 5-step flow because you're on Caddy.

Full walkthrough including per-engine notes (nginx / Apache / Traefik) and what to do if the manageability check refuses: [Migrate to Caddy](/help/migrate-to-caddy).

#### Option B — Keep your current engine; let Faro set up the domain in chat

Same end result for one domain. Slightly more clicks per recipe over time (the [[recipe-palette]] will mark Caddy-only flows with a 💬 **via chat** badge), but no migration required.

1. On the routing screen, click **Use the chat path**. The wizard closes and a pre-filled message lands in the chat composer at the bottom of the screen. The message looks roughly like *"I want to point newdomain.com at this server. DNS is already configured. Please set up HTTPS via my existing nginx + certbot."*
2. **Click Send.** Faro reads it and proposes a bundle of shell commands tailored to your engine. Each bundle pauses for your approval — you can read what's about to run before clicking Approve.
3. **Approve each bundle in order.** Typical sequence:
   - **For nginx:** Faro writes a new `/etc/nginx/sites-available/<domain>.conf`, symlinks it into `sites-enabled/`, runs `nginx -t` to validate, reloads nginx, then runs `certbot --nginx -d <domain>` which issues a Let's Encrypt cert and edits nginx config to terminate HTTPS. ~2–3 approvals.
   - **For Apache:** same shape with `/etc/apache2/sites-available/` + `a2ensite <domain>` + `apache2ctl configtest` + `systemctl reload apache2` + `certbot --apache -d <domain>` (or `httpd` paths on RHEL/Fedora). ~2–3 approvals.
   - **For Traefik:** Faro asks you which container should receive traffic for the new domain. Once you tell it (e.g. *"the whoami container"*), Faro proposes adding `traefik.http.routers.<routerName>.rule=Host(\`newdomain.com\`)` + `tls=true` labels to that container — either by editing your `docker-compose.yml` (Faro shows the diff) and running `docker compose up -d`, or by `docker container update --label-add` directly. ~1–2 approvals.
4. After the last bundle completes, Faro confirms the site is live: *"HTTPS is up at https://newdomain.com — open it in a new tab to verify."* Click the URL or paste it into your browser to check.
5. **Recovery if anything goes wrong.** Faro tells you which file changed at each step; you can scroll the chat back and click ↶ **Undo this change** on the offending bundle to roll back. Your existing sites stay unaffected because the bundles only add config; they don't edit what's already there.

The wizard's full DNS-write step (Cloudflare/Porkbun API integration) doesn't run on this path — you handle the DNS record yourself at your registrar before starting (Faro will tell you the exact A record to create if you ask). If your DNS is at Cloudflare or Porkbun and you want Server Manager to write the record for you, [migrate to Caddy](/help/migrate-to-caddy) first; the wizard's full automation only runs on Caddy servers.

#### Option C — Cancel and decide later

Not ready to choose? Click **Cancel** on the routing screen. The wizard closes, no changes were made, your existing engine keeps serving traffic. Come back whenever — Server Info → Web server will still show your engine + the Migrate button, and the wizard will land you back on this routing screen the next time you open it.

## Changing or removing a domain later

**Update the DNS record** — useful if you moved Server Manager to a different server and want this domain to follow. From the Done screen, expand *Need to change something?* → **Update the record**. The wizard writes a fresh A record pointing at the current server.

**Use a different domain** — same expanded section → **Start over with a different domain**. Pre-fills nothing; you type a new one.

**Remove the domain from this server.** No one-click "detach domain" button exists in the UI yet — the wizard only goes one way. But the chat path covers it cleanly. Two options:

- **If you're done with the whole site/app**, open the [[workload]]'s [[service-panel]] → **Controls** tab → **Delete**. This removes the workload *and* its Caddy block + cert in one step.
- **If you want to keep the site but detach this specific domain** (e.g., you want to move it to a different workload, or front it with a CDN instead), open the chat and ask Faro: *"remove the Caddy block for yourdomain.com — keep the workload otherwise."* Faro will edit `/etc/caddy/Caddyfile`, reload Caddy, and tell you what changed. The Let's Encrypt cert stays cached under `/var/lib/caddy/...` (harmless; Caddy garbage-collects unused certs eventually) and the workload keeps running on its auto-generated subdomain.

In **neither case** does Server Manager delete the DNS record at your DNS provider — do that yourself at Cloudflare/Porkbun/wherever if you want the domain to stop resolving here.

> **Special case — "broken" Caddy block.** If a workload was already deleted but its Caddy block was left behind (so the domain still resolves but nothing answers), opening the workload's panel surfaces a dedicated **Delete this site block** button on the broken-state tab. That's the only place a one-click "remove just the domain" button currently exists, and it's only there because that's a recovery scenario the UI explicitly handles.

## Reference

**Files touched on your server:**

- `/etc/caddy/Caddyfile` — one block added per connected domain
- `/var/lib/caddy/.local/share/caddy/certificates/` — Let's Encrypt cert + private key (managed by Caddy, don't edit manually)

**What goes over the wire:**

- DNS write call → Cloudflare or Porkbun API over HTTPS (token attached as header)
- Caddy reload → SSH command on your server
- Cert issuance → Caddy on your server talks to Let's Encrypt over HTTPS / ACME

**Supported DNS providers (direct API):** Cloudflare, Porkbun. Anything else: use the Cloudflare switch path (free, takes 5–60 min for propagation, domain stays where you bought it).

---

# Migrate from nginx, Apache, or Traefik to Caddy

URL: https://servermanager.dev/help/migrate-to-caddy
Category: Domains, HTTPS & email
Last updated: 2026-05-26

> One-click recipe that translates your existing reverse-proxy config to a Caddyfile, rehearses on alternate ports while the old engine keeps serving live traffic, then atomically swaps. Auto-rollback fires if anything fails verification. After migration, every Server Manager flow (deploy, connect-domain, install WordPress, …) works directly instead of routing through the chat fallback.

Server Manager is opinionated about [[caddy|Caddy]] — every recipe and wizard writes [[caddyfile|Caddyfile]] blocks, not nginx `server { }` or Apache `<VirtualHost>` or Traefik Docker labels. If your server is currently running one of those, recipes that need to touch proxy config fall back to a 💬 **via chat** path — they still work, but you click one extra approval per action. The **Migrate to Caddy** recipe takes you from that state to a fully-managed Caddy server in ~10 minutes, with a rehearsal step that lets Faro verify every site translates correctly before any real swap.

The recipe handles three source engines: **nginx**, **Apache** (both `apache2` on Debian/Ubuntu and `httpd` on RHEL/Fedora), and **Traefik** (Docker-labels provider). The shape of the migration is the same for all three — only the source-side details differ.

## 1. Open Server Info → Web server

In the top bar, click your server name to open the [[server-info|Server Info panel]], then click the **[[web-server-tab|Web server]]** tab.

![Server Info panel with the Web server tab open, showing nginx as the current engine and a Migrate to Caddy button](/help/migrate-to-caddy/01-web-server-tab.svg)

The tab shows:

- **Engine** — what's running on ports 80 / 443 right now (nginx / Apache / Traefik / Caddy / none).
- **Vhosts detected** — how many sites the engine is serving (Traefik shows "routers" instead).
- **TLS certificates** — who manages your HTTPS certs (typically `certbot` for nginx/Apache, Traefik's built-in ACME for Traefik).
- **Manageability** — a translatability check: a green ✓ means every site uses directives the recipe knows how to convert; a red ✗ tells you which directive is in the way (`mod_lua`, `proxy_cache_path`, complex `RewriteRule` chains, Traefik middlewares, etc.). If you hit a red ✗, the recipe refuses to run until you remove the unsupported directive — that's intentional, the recipe never silently loses behavior.

## 2. Click Migrate to Caddy

Click the **Migrate this server to Caddy →** button. Server Manager opens the chat panel and Faro takes over from there.

![Web server tab close-up: the Migrate this server to Caddy button is the destructive-marked CTA below the manageability check](/help/migrate-to-caddy/02-migrate-cta.svg)

The recipe runs in 5 phases. Phase 0 is read-only (just a verification pass). Phases 1–4 each pause for your approval before running any destructive command. You can cancel at any point — until Phase 4's atomic swap, your existing engine still owns the live ports and your sites keep serving traffic unchanged.

## 3. Approve each phase

Faro narrates what's about to happen before each approval. The bundles are short and focused.

- **Phase 0 — Pre-flight.** Read-only: enumerates your existing vhosts, checks for unsupported directives, finds the Let's Encrypt admin email. Ends with a plain-English plan + a `Reply: go` button. Click it (or type `go`) to start.
- **Phase 1 — Install Caddy.** Adds the official Caddy repo, installs the package, immediately stops the service. Your existing engine still owns the ports.
- **Phase 2 — Translate.** Writes a candidate Caddyfile to `/tmp` and validates it. Nothing live changes yet.
- **Phase 3 — Rehearse.** Starts Caddy on alternate ports `:8080` / `:8443` so it can be tested without touching real traffic. Faro then runs loopback `curl` tests itself (HTTPS, HTTP→HTTPS redirect, ACME challenge route) and shows you the response evidence per domain. Your existing engine still serves the real `:80` / `:443`.

![Phase 3 evidence: Faro's plain-English per-domain summary of the loopback curl results, ending with "rehearsal passed — ready for Phase 4 swap"](/help/migrate-to-caddy/03-rehearsal-evidence.svg)

- **Phase 4 — Atomic swap.** Backs up your existing config (`/etc/nginx.helm-backup.<timestamp>/` or `/etc/apache2.helm-backup.<timestamp>/` or `/tmp/traefik.helm-backup.compose.*.yml`), stops the old engine, starts Caddy on real ports `:80` / `:443`, verifies every domain with one more curl pass, then switches your existing `certbot.timer` from the engine-plugin renewal mode (`certbot --nginx`, `certbot --apache`) to webroot mode that Caddy can serve. The `certbot.timer` keeps owning renewal; a deploy-hook reloads Caddy on each renewal so you never need to touch it.

> **If verification fails at any point in Phase 4**, the recipe auto-rolls back: stops Caddy, restarts the old engine, leaves your existing config untouched. You're back to the pre-migration state in <10 seconds.

## 4. Confirm + handoff

After Phase 4 succeeds, Faro asks you to open Server Info → Web server one more time to confirm.

![Post-migration Web server tab: engine shows Caddy (host service), TLS shows "certbot (Let's Encrypt, renewed by certbot.timer; served by Caddy via deploy-hook reload)", Status shows the green ✓ end-to-end](/help/migrate-to-caddy/04-after-migration.svg)

The tab refreshes itself on open (no disconnect/reconnect needed). You should see:

- **Engine: Caddy (host service)**
- **TLS certificates** — for nginx/Apache migrations: `certbot (Let's Encrypt, renewed by certbot.timer; served by Caddy via deploy-hook reload)`. For Traefik migrations: `Caddy ACME (automatic Let's Encrypt)` — Traefik's old `acme.json` certs aren't reused; Caddy obtains fresh ones during the cutover.
- **Status: ✓ Server Manager manages this server end-to-end.** Every recipe and wizard works directly; nothing routes to chat-only fallback.

Your old engine package stays installed (but disabled) for ~30 days as a manual rollback option. The config backup also stays on disk. Once you're confident the migration is solid (~1 week), you can `apt remove nginx` / `apt remove apache2` to free a few MB, or stop the old Traefik container with `docker rm traefik`.

## Per-engine notes

The rehearse-then-swap shape is the same across engines. The differences are mechanical:

**nginx.** Source-engine probe via `sudo nginx -T`. Backup at `/etc/nginx.helm-backup.<ts>/`. Renewal handoff: `authenticator = nginx` → `authenticator = webroot` in `/etc/letsencrypt/renewal/<domain>.conf`. The `certbot --nginx` plugin stops being used; `certbot.timer` keeps running.

**Apache.** Same shape with paths swapped for `apache2` (Debian) or `httpd` (RHEL). Vhost enumeration via `apache2ctl -S` + reading `sites-enabled/` (or `conf.d/` on RHEL). Backup at `/etc/apache2.helm-backup.<ts>/`. Renewal handoff: `authenticator = apache` → `webroot`. `mod_php` setups are flagged unmanageable until you remove them — Caddy doesn't run PHP in-process the way Apache + mod_php does.

**Traefik.** Source-engine is a Docker container, stopped with `docker stop traefik` (not `systemctl stop`). Vhosts come from `traefik.http.routers.*` labels on running containers, not from config files. **Cert reuse is skipped** — Caddy obtains fresh Let's Encrypt certs via auto-HTTPS during the cutover, costing one fresh issuance per domain (~30–60 second cert window). Routers with custom middlewares or non-`Host()` matchers are flagged unmanageable. Backends must have host-published ports (e.g. `127.0.0.1:8080:80` in the compose file) — Caddy as a host service can't reach Docker-network-only containers.

## What if the recipe refuses?

If the manageability check is a red ✗, Faro names the exact directive and vhost. Typical fixes:

- **nginx `proxy_cache_path` / `fastcgi_cache`** — these don't translate 1:1 to Caddy. Remove the cache zone (or move caching to a CDN like Cloudflare) and retry. Most non-Caddy users on small servers don't need on-box caching.
- **Apache `RewriteRule ... [L,R=301]`** — flag bundles indicate multi-step chains the translator can't model. Convert to a simpler `Redirect permanent`, or pre-flatten the chain.
- **Traefik middlewares** — middlewares need per-type Caddy translation that the recipe doesn't auto-do in v1. Remove the middleware labels and apply the equivalent in Caddy after migration (basic auth, rate limit, headers — all supported, just not auto-translated).
- **Traefik `HostRegexp` rule** — regex matchers aren't 1:1 translatable. Switch to one or more `Host()` rules with explicit hostnames.

If you can't simplify your config and don't want to migrate, **the chat path still works for everything Server Manager would normally do via recipes** — deploy a site, install WordPress, connect a domain, set up a database, take a backup, restore from one, etc. Faro reads your existing engine and proposes the right native commands (`/etc/nginx/sites-available/` + `certbot --nginx` for nginx, the Apache equivalent, Docker labels for Traefik). Concrete example asks that work today on a non-Caddy server:

- *"Add a new WordPress site at blog.mysite.com on this nginx server"* — Faro proposes the nginx vhost + the docker compose for WordPress + certbot. You approve each bundle.
- *"My TLS cert for api.mysite.com is about to expire — check it and renew if needed"* — Faro runs the inspection + the renewal + the reload, all in chat.
- *"Take a backup of the api.mysite.com workload"* — Faro figures out what to dump + how to package it + offers the download link.

You don't lose features by staying on your current engine. You trade one-click UI affordances for chat-mediated equivalents.

## Reference

**Files written on your server during migration:**

- `/etc/caddy/Caddyfile` — Caddy's site config (new on this server)
- `/var/www/certbot/.well-known/acme-challenge/` — webroot for ACME renewals (nginx + Apache; not used for Traefik migrations)
- `/etc/letsencrypt/renewal-hooks/deploy/reload-caddy.sh` — reloads Caddy on each certbot renewal (nginx + Apache only)
- `/etc/letsencrypt/renewal/<domain>.conf` — surgically edited to swap `authenticator = nginx` / `authenticator = apache` → `authenticator = webroot` (nginx + Apache only)

**Files / state preserved for rollback:**

- nginx: `/etc/nginx.helm-backup.<timestamp>/` (full config copy)
- Apache: `/etc/apache2.helm-backup.<timestamp>/` (Debian) or `/etc/httpd.helm-backup.<timestamp>/` (RHEL)
- Traefik: `/tmp/traefik.helm-backup.compose.<timestamp>.yml` (the docker-compose file), plus the stopped Traefik container itself
- Old engine package stays installed but disabled (`systemctl disable apache2` / `nginx`; `docker update --restart=no traefik`)

**Approval gates:** typically 4 clicks for nginx and Apache, 5 for Traefik (the extra one is when Faro needs to pick a non-default rehearsal port if your backend already uses `:8080`).

---

# Set up email for your domain

URL: https://servermanager.dev/help/set-up-email-for-your-domain
Category: Domains, HTTPS & email
Last updated: 2026-07-13

> Two separate surfaces — Send (apps emailing FROM your domain) and Receive (mail addressed TO your domain). Walks through both wizards and explains which one you need.

Email at your domain is **two** separate concerns. Server Manager has a wizard for each.

| Goal | Wizard | What it does |
|---|---|---|
| Apps on this server send mail like `noreply@yourdomain.com` (password resets, welcome emails…) | **Send email from your domain** | Configures Resend + writes SPF / DKIM / DMARC DNS records |
| Mail addressed `me@yourdomain.com` lands somewhere you can read it | **Receive email at your domain** | Either forwards to your Gmail/Outlook (free), or routes to a paid mailbox provider |

You can run one, the other, or both. They don't depend on each other. Both wizards live under the top bar's [[set-up-menu|**Set up**]] menu — click that term to see exactly where it is.

> **Prerequisite for both:** DNS for your domain must already be at Cloudflare or Porkbun. If it isn't, run [Connect a domain](/help/connect-a-domain) first — that wizard's Cloudflare-switch path is the standard route here too.

## Why not just use a mail hosting service?

A reasonable question — and the answer is: you can, and these wizards still help. Mail hosting services and Server Manager's email wizards solve overlapping but different problems. Quick map:

| | **Mail hosting service** | **Send wizard** | **Receive wizard** |
|---|---|---|---|
| Examples | Google Workspace, Microsoft 365, Fastmail, Zoho, Migadu | [Resend](https://resend.com/) (only adapter today) | Cloudflare Email Routing (Branch A); your mailbox host (Branch B) |
| What it is | A paid product running a full mailbox + (usually) calendar, docs, drive | A short wizard that wires up Resend to send mail from your domain | A short wizard that routes mail addressed to your domain |
| What it does | Webmail, mobile sync, sending and receiving as a human, calendar / files | Lets apps on your server send email like `noreply@yourdomain.com` (password resets, receipts) | Forwards mail to your existing Gmail/Outlook (free, Branch A) OR points DNS at any mailbox host you signed up for (Branch B) |
| Cost | $3–12 / user / month | Free up to ~3000 emails / month at Resend | Free (Branch A) or $1–6 / mailbox / month at the provider (Branch B) |
| Where mail lives | At the mail host | Resend just sends — no inbox of its own | Wherever you forwarded it / signed up |
| Best when | You want one paid suite for email + calendar + docs across a team | An app on your server needs to email humans | You want mail at your domain to be **readable** without paying for a whole productivity suite |

**Common combos:**

- **Just transactional mail** — your app sends order receipts; nobody emails you back. Run **only the Send wizard**.
- **Hobby / solo** — you want `you@yourdomain.com` to feel professional, but you already live in Gmail. Run **Send + Receive Branch A** (free, forwards into your existing Gmail).
- **Small business, no Workspace** — multiple people need real mailboxes at the domain. Pick a paid mailbox provider, run **Send + Receive Branch B** (DNS paste).
- **Already on Workspace / M365** — you don't need the Receive wizard at all (Workspace handles mailboxes). You may still want the **Send wizard** so apps don't have to go through Workspace's SMTP relay, which has tight rate limits and isn't designed for app-sent mail.

## Part 1 — Send email from your domain (outbound)

This is what you want when **an app or website running on your server needs to send email**. Password reset emails, "your order shipped" notifications, contact-form submissions, welcome mails.

Server Manager uses **Resend** as the delivery provider. Resend has a generous free tier (100 emails/day, 3000/month) and a clean API. Your app calls Resend's `/emails` endpoint with a `from: noreply@yourdomain.com` header; Resend handles delivery, bounce tracking, and the SPF/DKIM signing that keeps you out of spam.

### 1. Open the wizard

Top bar → **Actions** → **Send email from your domain**.

![Actions menu with Send email from your domain highlighted](/help/set-up-email-for-your-domain/01-open-send.svg)

### 2. Pick the provider

Resend is selected by default — leave it. (Postmark / Mailgun / SES are planned; for now Resend is the only adapter.)

![Stage 0 — Resend selected as the provider](/help/set-up-email-for-your-domain/02-pick-provider.svg)

### 3. Paste your Resend API key + domain

Open [resend.com/api-keys](https://resend.com/api-keys) (sign up free if you don't have an account) and create a new API key. Copy it.

In the wizard, type the domain you want to send FROM (e.g. `yourdomain.com`), paste the key, hit **Continue**.

![Stage 1 — Resend API key + domain inputs](/help/set-up-email-for-your-domain/03-paste-resend-key.svg)

### 4. Server Manager writes three DNS records

Resend issues three records — an **SPF** TXT, a **DKIM** TXT, and a **DMARC** TXT. The wizard writes them at your DNS provider (Cloudflare or Porkbun) using the token from Connect-a-domain (or asks for one if it doesn't have it yet).

![Stage 3 — three DNS records being written](/help/set-up-email-for-your-domain/04-write-records.svg)

> **What are SPF / DKIM / DMARC?** Three TXT records that prove to receiving mail servers (Gmail, Outlook, …) that mail claiming to be from your domain actually was sent by Resend on your behalf. Without them, your mail lands in spam — Gmail in particular is strict. You don't need to understand the protocols; Server Manager just needs to write them where Resend told it to.

### 5. Wait for Resend to verify

DNS usually propagates in 1–5 minutes. Resend re-checks on a schedule; the **Check now** button nudges them to look right away.

![Stage 4 — verifying with Status: pending / verified](/help/set-up-email-for-your-domain/05-verifying.svg)

You can **close the wizard and come back later** — the records are already in place; verification continues in Resend's background. When you reopen the wizard with the same domain + key, it jumps straight to Done if Resend has caught up.

### 6. Done — how to actually send mail

The success screen shows you the exact API call to use:

```
POST https://api.resend.com/emails
Authorization: Bearer <your-api-key>
Content-Type: application/json

{
  "from": "noreply@yourdomain.com",
  "to": "user@example.com",
  "subject": "...",
  "html": "..."
}
```

Plug that into your app's code (the [Resend Node/Python/PHP SDKs](https://resend.com/docs) wrap this). Keep the API key in an environment variable, not in source code.

![Stage 5 — success screen with API example](/help/set-up-email-for-your-domain/06-send-done.svg)

## Part 2 — Receive email at your domain (inbound)

This is what you want when **someone sends an email to `you@yourdomain.com` and you want it to land somewhere readable**. Two real ways to do it; pick at the start.

### 1. Open the wizard

Top bar → **Actions** → **Receive email at your domain**.

![Actions menu with Receive email at your domain highlighted](/help/set-up-email-for-your-domain/07-open-receive.svg)

### 2. Pick the path

After you type the domain, the wizard offers two cards:

![Pick-branch screen with two cards: Forward / Real mailbox](/help/set-up-email-for-your-domain/08-pick-branch.svg)

**Forward to my existing inbox** *(free, automated)* — mail to `me@yourdomain.com` lands in your existing Gmail/Outlook/Yahoo/iCloud/etc. Powered by **Cloudflare Email Routing**. End-to-end automated; the wizard does everything except clicking a verification link.

**Real mailbox at my domain** *(paid, you pick the provider)* — a full email account at `me@yourdomain.com`. Webmail, mobile sync, calendar, send-from-the-domain natively. You sign up + pay at an email-hosting provider (1–6 $/month/mailbox); Server Manager helps with the MX record paste.

### Quick comparison

| | Forward to Gmail | Real mailbox |
|---|---|---|
| Cost | Free | $1–6 / month / mailbox |
| Where mail lives | Your existing inbox | At the new provider |
| Replies come from | Your existing address (Gmail "send-as" can fix this — extra setup) | Your domain, natively |
| Setup time | ~5 min | ~10–15 min |
| What you need | A Cloudflare API token | An account at any email-hosting provider |

If you mainly want emails to your domain to be **readable**, Forward is the right call. If you want clients to see replies coming from your domain naturally (and you don't mind paying), Mailbox.

### Branch A — Forward to my existing inbox

#### A1. One-time enable at Cloudflare

Before pasting the token, the wizard tells you to enable Email Routing in Cloudflare's dashboard once:

1. Open [dash.cloudflare.com](https://dash.cloudflare.com/) → **Compute** → **Email Service** → **Email Routing** in the left sidebar.
2. Click **Onboard Domain** and pick your domain.
3. Cloudflare shows the DNS records it will add — an **MX** set plus **SPF** and **DKIM** TXT records. Confirm (it may ask to replace existing MX records), then **Done**.
4. Wait ~5–15 minutes until Email Routing shows as **enabled** for your domain.

This one-time onboarding has to happen in CF's UI; their API rejects the enable call until it's done. Cloudflare retired the old **Get Started** screen in mid-2026 — if your account still shows it, a **Use the old UI** banner toggle exists, but the Onboard Domain flow above is what most accounts now see.

#### A2. Create a Cloudflare API token

The wizard shows you the exact token setup, but here's the gist:

- Open [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens) → **Create Token** → **Create Custom Token**.
- Add **four permissions**: `Zone:Read`, `Zone:DNS:Edit`, `Zone:Email Routing Rules:Edit`, `Account:Email Routing Addresses:Edit`.
- Under **Zone Resources**, restrict to your domain.
- Create the token; copy it before navigating away (CF only shows it once).

Paste it in the wizard.

![Branch A — paste Cloudflare token with the permission checklist visible](/help/set-up-email-for-your-domain/09-fwd-paste-token.svg)

#### A3. Configure the forward

The wizard loads your Cloudflare zones and asks two things:

- **Forward this address** — type a name like `me`, `hello`, or `info`. Use `*` to catch **every** address at your domain (mail to anything@yourdomain.com forwards).
- **Forward to this inbox** — any email you can read: Gmail, Outlook, Yahoo, iCloud, ProtonMail, Fastmail, a work address. Doesn't have to be Gmail. **Don't use an address at the same domain** — that loops.

![Branch A — configure forward: localPart + destination](/help/set-up-email-for-your-domain/10-fwd-configure.svg)

#### A4. Verify the destination inbox

Cloudflare sends a verification email to the destination. Open that inbox, find the message from Cloudflare ("Verify your email address"), click the link, come back, click **Check now**.

![Branch A — verify destination instructions](/help/set-up-email-for-your-domain/11-fwd-verify.svg)

If the destination is the same address as your Cloudflare account's own email, it's auto-verified and this step is skipped.

You can close the wizard mid-verify and come back later — Cloudflare keeps the verification link valid for a while; the rule gets created automatically when you click Check now after verifying.

#### A5. Done

Send a test mail from any other inbox to `me@yourdomain.com` — it should arrive in your destination inbox within ~1 minute. (Check Spam — first deliveries from a new domain sometimes land there until the receiving provider sees a clean pattern.)

> **What if Cloudflare's API rejects the token with "Authentication error"?** Their Email Routing API occasionally fails the enable / rule-create call even when the token is correct — three known gotchas. The wizard surfaces a **Stuck?** disclosure with manual fallback steps you can follow in Cloudflare's dashboard. Same end result.

### Branch B — Real mailbox at my domain

#### B1. Sign up at a provider

Pick an email-hosting provider that fits your needs and price point. Search "email hosting" — there are many. Common picks people land on offer webmail, mobile sync, IMAP/SMTP, and good deliverability. Server Manager doesn't recommend a specific one because the right choice depends on your jurisdiction, language, support needs, and integrations.

Once you have an account, the provider's onboarding will ask you to **add your domain**. After you do, they'll show you 1–3 **MX records** that need to be added to your DNS. Copy them.

![Branch B — explainer screen with the trade-offs](/help/set-up-email-for-your-domain/12-mb-explainer.svg)

#### B2. Paste the MX records

In the wizard, paste each MX record's **priority** (usually `10`, `20`) and **server hostname** (e.g. `mx.provider.com`). Most providers list 1 or 2; some list 3. Use **+ Add another MX record** for more rows.

![Branch B — three MX-record input rows](/help/set-up-email-for-your-domain/13-mb-paste-mx.svg)

Below the rows, the wizard auto-detects your DNS provider (Cloudflare or Porkbun) and asks for the matching API token. The token + key flow is identical to [Connect a domain](/help/connect-a-domain) — same provider, same kind of token.

Click **Write MX records**. Each row shows ✓ or ✗ as it gets written.

#### B3. Verify at the provider

The wizard's done — the MX records are live. Final step happens at **your provider**: they may take a few minutes to detect the MX change and mark your domain as verified. Log into their dashboard and check the domain's verification status. Once verified, create mailboxes (like `me@yourdomain.com`) at the provider and start receiving mail.

![Branch B — done screen pointing back to the provider for verification](/help/set-up-email-for-your-domain/14-mb-done.svg)

> **Mailbox providers don't typically expose a sending API for your apps.** If you also want apps on this server to **send** email from this domain, run Part 1 (Send email from your domain) separately — the two coexist fine. The mail server (provider) handles inbound; Resend handles outbound from your apps.

## Common questions

**Can I use Gmail Workspace / Microsoft 365 instead?** Yes — they're standard mailbox providers. Sign up there, get the MX records they give you, paste them in Branch B. Server Manager doesn't have a special integration for either, but the generic mailbox flow works.

**Can I keep using my old email setup and just send from the domain?** Yes — Part 1 (outbound) and Part 2 (inbound) are independent. Set up outbound only; leave inbound MX records pointing wherever they already do.

**What happens to existing MX records if I run Branch A?** Cloudflare Email Routing **replaces** them (the dashboard onboarding asks you to confirm this). If you currently receive mail somewhere else and want to keep it, choose Branch B and add the existing provider's MX records.

**Will mail land in spam?** First mail from a fresh domain often does, especially if the receiving provider is Gmail. The SPF/DKIM/DMARC records that Part 1 writes are exactly what's needed to graduate out of spam — once you've sent a small volume of real-looking mail (not transactional-looking-but-bulk), Gmail starts trusting the domain.

**Can I revoke a token after setup?** Yes. The Cloudflare/Porkbun/Resend tokens are only needed **during** the wizard; once records are written and your domain is verified, you can delete the tokens at the provider's dashboard. Server Manager never stored them.

## Reference

**DNS records written by each wizard:**

- **Send email** (Part 1) — 3 TXT records: SPF, DKIM, DMARC (provider-issued)
- **Receive — Forward** (Branch A) — 3 MX records (Cloudflare Email Routing's), plus the routing rule (in CF, not DNS)
- **Receive — Mailbox** (Branch B) — 1–3 MX records (provider-issued)

**Tokens / keys used (none are persisted by Server Manager):**

- Resend API key (`re_…`) — for the Send wizard
- Cloudflare API token — for both Send (DNS write) and Receive Branch A (Email Routing)
- Porkbun API key + secret (`pk1_…` / `sk1_…`) — for DNS write when domain is on Porkbun

**Status is per-host, in your browser.** Whether email is "set up" for a domain is tracked by markers each wizard writes to browser localStorage on success — a UX hint, not source of truth. Resend / your DNS provider are authoritative.

---

# Restore from a backup

URL: https://servermanager.dev/help/restore-from-a-backup
Category: Backups & recovery
Last updated: 2026-05-26

> How to bring back a `.tar.gz` bundle you previously downloaded from Server Manager — either in place (overwrite the original) or as a clone at a new domain. Walks the three entry points and the chat handoff.

You have a `.tar.gz` bundle you saved earlier (or one a teammate sent you, or one that just streamed in from another server). This walks through how to bring it back as a running service — either **in place** (overwriting the original if it's still there) or as a **clone** at a new domain.

> **Looking for the making-backups side?** That's a separate article: [Backups — which one do I want and how to use it](/help/backups). This one starts from "I already have a bundle on my computer."

## Where to start the restore — three entry points

Same flow, three doors. Pick whichever matches where you're starting from. (Two of the doors use the top-bar [[set-up-menu]] — click that term to see where it lives.)

| Where you are | Door |
|---|---|
| You know which workload it is, and there's an existing service panel for it | Open the service panel → **Backup** tab → **Restore from a bundle** |
| You don't have a matching workload yet (e.g. fresh server, or the original was deleted) | Top bar → **Actions** → **Restore from a backup** |
| You just used **Move to another server** and the bundle landed here | The Restore modal opens automatically with the just-transferred bundle preselected |

The three doors all open the same **Restore from a backup** modal. The only difference is what context Server Manager has at the moment you open it:

- From a **service panel**, the modal knows which recipe + workload you expected — if the uploaded bundle is a different shape (e.g. you opened the WordPress panel but uploaded a Postgres bundle), it shows a warning so you can pick a different file before anything runs.
- From the **Actions** menu, no expectation is set — whatever the bundle says it is, that's what gets restored.
- From an **incoming transfer**, the bundle is already on the server; the modal offers a one-click **Restore this bundle** button.

## The walk-through

### 1. Open the Restore modal

Pick whichever entry point fits your situation (see the table above). The modal looks the same in all three cases:

![Restore from a backup modal — drag-drop zone with "Drop a bundle here, or click to choose" and Cancel / Upload buttons](/help/restore-from-a-backup/01-open-modal.svg)

### 2. Pick the bundle

Drag the `.tar.gz` file onto the drop zone, or click anywhere in the zone to open a file picker. The modal accepts files ending in `.tar.gz` or `.tgz`.

![Drop zone with a bundle filename selected and a size pill underneath](/help/restore-from-a-backup/02-bundle-selected.svg)

Click **Upload**. A progress bar replaces the drop zone while the bundle streams to the server (chunked upload — even multi-GB bundles survive flaky connections).

![Upload progress bar showing 64% complete](/help/restore-from-a-backup/03-uploading.svg)

> **What's being uploaded?** Your computer sends the `.tar.gz` bytes to the server over an SFTP-style stream — encrypted by the SSH session you're connected over. The file lands at `/tmp/helm-restore/<id>/<bundle>.tar.gz` and stays there until the restore finishes (then it's cleaned up).

### 3. The chat takes over

When the upload finishes, the modal closes and Faro greets you in the chat with the manifest summary it read out of the bundle: the source title, the source domain, the recipe, and the timestamp of the backup. Then it asks you **one short question**:

> Do you want to **restore in place** (overwriting the existing `<name>` if it's still present) or **clone to a new domain** (keeping the original untouched, creating a separate copy)?

![Chat: Faro asks "restore in place or clone to a new domain?" after reading the bundle](/help/restore-from-a-backup/04-chat-question.svg)

This is the only decision point. From here, your answer determines the rest of the steps.

### 4a. Restore in place

Pick this when:

- The original workload is gone (deleted, or you're on a fresh server) and you want to bring it back at the **same domain** with the same names.
- The original is broken / misconfigured and you want to wipe it and re-create from the bundle.

What Faro does:

1. Reads `docker-compose.yml` and `.env` out of the bundle and re-creates them under the original install path.
2. Restores every named Docker volume (database data, uploaded files, etc.) from the volumes in the bundle.
3. For static sites, copies the file tree back under `/var/www/<domain>/`.
4. Re-applies the Caddyfile block so the original domain serves again.
5. Starts the service and runs a health check.

Every command pauses for your approval before it runs — the chat shows the exact `docker compose up`, `tar xzf`, `caddy reload` lines you're about to execute.

### 4b. Clone to a new domain

Pick this when:

- You want the original to keep running while a copy goes up at a different domain (staging, dev, demo, second business…).
- You're restoring onto a server that has a different domain pointed at it than the bundle's source.

Faro asks one extra question: **what's the new domain?** (e.g. `staging.example.com`).

![Chat: Faro asks for the new domain when cloning](/help/restore-from-a-backup/05-clone-prompt.svg)

What Faro does on top of the in-place steps:

1. Picks a **fresh compose project name** (so the clone's containers don't collide with the original's), e.g. `mysite-com` → `staging-example-com`.
2. **Rewrites the Caddyfile** entry to use the new domain.
3. **Rotates secrets** in `.env` (new DB password, new app keys) — the clone doesn't share credentials with the original.
4. For **WordPress**, runs `wp search-replace <old-domain> <new-domain>` inside the cloned container so links inside posts/media metadata point at the new domain.
5. For **web apps**, updates any `*_URL` / `*_HOST` env vars pointing at the old domain.
6. Requests a new TLS certificate for the new domain (Let's Encrypt via Caddy, automatic).

The original keeps running untouched.

> **Cloning a database?** Databases don't have a domain. Faro asks for a **short suffix** instead (like `staging` or `qa`) and uses it to derive a unique compose project name, container name, and listening port so both DBs run side-by-side without colliding.

### 5. Confirm it's up

When the steps finish, Faro shows the final URL (clone) or the now-restored domain (in place). Open it in your browser; if it doesn't load straight away, give Let's Encrypt 30–60 seconds to issue the cert and try again.

![Workload card showing the restored or cloned service back on the home screen](/help/restore-from-a-backup/06-done.svg)

The original bundle is removed from `/tmp/helm-restore/` automatically once the restore finishes successfully.

## Recipe-shape mismatch

If you opened the Restore modal from a service panel (e.g. the WordPress panel for `mysite.com`) and uploaded a bundle of a **different recipe** (e.g. a Postgres bundle), the modal won't just go ahead — it warns you:

![Mismatch warning — uploaded bundle is a different recipe than the panel you opened it from](/help/restore-from-a-backup/07-mismatch.svg)

You have two choices:

- **Pick a different file** — almost always what you want (you grabbed the wrong bundle).
- **Use this bundle anyway** — the bundle drives the restore, so this will set up a fresh service of whatever the bundle's recipe is, NOT modify the one whose panel you opened. Pick this only if you understand that and want it.

The mismatch check only triggers when the modal was opened from a service panel with an expected recipe. The **Actions → Restore** path skips it entirely (no expectation to check against).

## Auto-restore after a server-to-server move

If you used **Backup tab → Move to another server** on another server and it transferred a bundle here, the Restore modal opens automatically with that bundle highlighted at the top:

![Restore modal with the preload banner — "A backup bundle is ready on this server" — and a Restore this bundle primary button](/help/restore-from-a-backup/08-preload.svg)

Click **Restore this bundle** and the rest of the flow is identical to a fresh restore from there on (Faro asks restore-in-place vs. clone, you approve commands, etc.). The bundle is already on the server, so there's no upload step.

You can also click **Use a different bundle…** to dismiss the preload and open the regular file picker — useful if you transferred the wrong bundle and want to start over.

## Common questions

**Where does the original live during a clone?** Untouched. The clone uses different container names, different volumes, different ports (for databases), and different secrets. You can delete the clone later without affecting the original.

**Can I restore a bundle onto a different Linux distro?** Yes. The bundle is just Docker compose + named volumes + (for static sites) a file tree. Server Manager handles the per-distro Docker install on the target if it's missing. The exception is **native** (non-container) web-app bundles — those depend on the source distro's package manager and systemd, and the restore article will tell you if it can't translate the install.

**The bundle is huge — will the upload time out?** No. The upload is chunked (~4 MB per chunk) and the server pieces them together. Multi-GB bundles work; if your connection drops mid-upload, the modal lets you cancel and start over rather than restarting from zero on every chunk.

**What if I cancel mid-restore?** Cancel during **upload** deletes the partial file and you're back to the drop zone. Cancel during the **chat steps** (between approvals) leaves whatever Faro had already done in place — some containers may exist, some volumes may exist. You can re-run the restore from the same bundle, or have Faro clean up the partial state first; if anything looks weird, ask in chat and Faro will diagnose.

**Can I restore the same bundle multiple times to different clones?** Yes. Re-open the modal, upload the same bundle, pick **clone** each time with a different new domain. The bundle stays valid forever (it's just a tarball).

**Where do failed restores leave files?** `/tmp/helm-restore/<id>/` on the target server. The home screen's [[set-up-menu|Actions]] surface shows orphan helm-restore directories in the cleanup view if any are left around. Safe to delete.

## What's NOT in scope here

- **`.helm-backup/` files** (per-file undo in the Files tab) → see the [Backups](/help/backups) article's "Recover a single file" section.
- **`.sql.gz` raw DB dumps** (loadable into any Postgres/MySQL) → see the [Backups](/help/backups) article's "Back up just the database" section.
- **Restoring across recipe boundaries** (e.g. a WordPress bundle as a static site) — bundles are recipe-specific. Pick a bundle that matches what you want to bring up.

## Reference

**What restore needs on the target server:**

- The SSH-reachable Linux server you're connected to in Server Manager.
- Disk space for the bundle extract (roughly 2× the bundle size, briefly, during extraction).
- Docker installed (Server Manager installs it automatically if missing on a fresh target).

**Disk paths during restore:**

- Uploaded bundle (transient) → `/tmp/helm-restore/<id>/<name>.tar.gz`
- Extracted source → `/tmp/helm-restore/<id>/extracted/`
- Final install — same path the original used (e.g. `/opt/wordpress-mysite-com/`, `/var/www/mysite.example.com/`, …)

**The decision tree at a glance:**

| Situation | Pick |
|---|---|
| Original gone, want it back as-is | **Restore in place** |
| Original broken, want a clean reinstall from the bundle | **Restore in place** |
| Want a copy at a different domain | **Clone to a new domain** |
| Want a copy of a database alongside the original | **Clone alongside** (database-only — uses a suffix instead of a domain) |
| Just received a bundle from a Move-to-another-server transfer | Click **Restore this bundle** in the preload banner |

---

# Migrate an existing site here

URL: https://servermanager.dev/help/migrate-an-existing-site
Category: Deploy & manage sites and apps
Last updated: 2026-05-26

> Pull a WordPress site, a static site, or a database off your old host (SSH or cPanel) and stand it back up on this server. Source stays untouched; the actual install happens in the chat with your approval.

Coming to Server Manager from a different host? The **Import from another host** wizard logs into your old server, scans it for WordPress sites, static sites, and databases, then copies one of them over as a `.tar.gz` bundle. The bundle then flows into the [Restore wizard](/help/restore-from-a-backup) which actually installs the service at this server, with your approval on each step.

> **The source is read-only.** Nothing on your old host gets modified. The wizard scans, copies what it needs to, and disconnects. You can re-run it later to migrate another site.

## What can I migrate?

The wizard recognizes three shapes of "thing to move":

| Kind | What it finds | What ends up here |
|---|---|---|
| **WordPress site** | A `wp-config.php` + database it can reach (auto-fills credentials when readable) | Full containerized WordPress install with a fresh database, your media + plugins + themes restored |
| **Static site** | A document root with `index.html` (under `/var/www/`, `/home/<user>/public_html/`, common cPanel layouts…) | The file tree under `/var/www/<your-new-domain>/`, served by Caddy |
| **Database** | A MySQL / MariaDB instance reachable from the source's SSH login | A new database container with a `mysqldump`-restored copy |

It does **not** migrate: arbitrary web apps (Node/Python/PHP frameworks beyond WordPress — those need [Deploy from a git repo](/help/deploy-web-app-from-a-git-repo) instead), email/mailboxes, DNS records, or anything outside the standard webroot/database conventions.

## How to reach your old host — SSH or cPanel

The wizard supports two ways to connect:

| Source type | When to pick it |
|---|---|
| **SSH login** | You have shell access (the same login you'd use with the `ssh` command). Most VPS providers give you this by default. |
| **cPanel API** | You only have cPanel and no SSH (common on shared hosting). Uses cPanel's own API token instead of a Linux login. |

If you're not sure, try SSH first — it's the lower-friction path. Switch tabs if the connection fails or your host only offers cPanel.

## The walk-through

### 1. Open the wizard

Top bar → [[set-up-menu|**Actions**]] menu → **Import from another host**.

![Actions menu with "Import from another host" highlighted](/help/migrate-an-existing-site/01-open-modal.svg)

### 2. Connect to your old host

Pick the source type, fill in the credentials, click **Scan the source**.

**SSH login mode** wants: host (IP or domain of the old server), port (usually 22), username (`root`, `ubuntu`, your cPanel user, etc.), and either a password or an OpenSSH private key. Got the same credentials saved as a Server Manager server already? Click **Use credentials from a saved server** to pre-fill from there.

![SSH login source mode — host, user, port, password fields](/help/migrate-an-existing-site/02-creds-ssh.svg)

**cPanel API mode** wants: the full cPanel URL (including port — usually `https://your-domain:2083`), your cPanel username, and an API token. Get the token from cPanel → search "Manage API Tokens" → **Create** — cPanel only shows it once, so copy + paste right away.

![cPanel API source mode — URL, username, token fields](/help/migrate-an-existing-site/03-creds-cpanel.svg)

> **What the wizard does with credentials.** They stay in this browser session only and are sent over HTTPS direct to the source. We don't write them to disk on this server or store them between wizard runs.

### 3. Scan and pick

The wizard logs into the source and walks its common web/database paths for ~10–30 seconds, grouping anything it recognizes by kind.

![Scanning state with a progress bar](/help/migrate-an-existing-site/04-probing.svg)

Pick **one** item to migrate this run (multi-select isn't a thing yet — re-open the wizard for the next one). The selected card gets a peach outline.

![Pick stage — found WordPress sites, static sites, databases grouped; one selected with peach outline](/help/migrate-an-existing-site/05-pick.svg)

If the scan returns nothing, the wizard tells you which roots it checked + any warnings (permission errors, unreadable wp-config, etc.). Adjust the source credentials or path layout if the missing item is on a non-standard root.

### 4. Confirm the destination

This step varies by kind:

- **WordPress** → pick the domain to host the migrated site at (can be the same as before — you switch DNS after — or a brand-new one), and confirm the DB credentials (pre-filled from `wp-config.php` if readable).
- **Static site** → optionally pick a domain. Leave blank to serve at your server's IP for now; you can attach a domain later from the service panel.
- **Database** → pick a title for the new database on this server, and (rarely) override `mysqldump` credentials.

![Configure stage for a WordPress migration — destination domain + DB host/name/user/password fields](/help/migrate-an-existing-site/06-configure-wp.svg)

Click **Start the import**.

### 5. The transfer

Server Manager pulls the data from the source and assembles a Server-Manager-format bundle on this server. Progress lines update in real time (creating archive, transferring, hashing, etc.). The source side is just reads — no writes, no temp files left behind.

![Importing stage with progress bar and step labels](/help/migrate-an-existing-site/07-importing.svg)

> **Don't close the dialog mid-import.** If you do, the in-flight transfer is aborted and you'll need to restart. Cleanup happens automatically — partial files at `/tmp/helm-restore/` on this server get cleaned up within 24 hours, or you can clean them immediately from the error screen if something fails.

### 6. Hand-off to the Restore wizard

Once the bundle is assembled, the Migrate modal closes and the [Restore wizard](/help/restore-from-a-backup) opens with the just-arrived bundle preselected. It's the same modal the [Move to another server](/help/backups) flow uses on arrival, with the same one-click **Restore this bundle** button.

![Restore modal preload state — "A backup bundle is ready on this server" with Restore this bundle highlighted](/help/migrate-an-existing-site/08-handoff.svg)

Click **Restore this bundle** and the chat takes over: Faro reads the manifest, kicks off the install at this server, and pauses for your approval on each command — `docker compose up`, the Caddyfile edit, the `wp search-replace` if the domain changed, etc.

When it finishes, the migrated site shows up as a workload card on your home screen — same as anything else you'd deployed natively here.

### 7. (Optional) Switch DNS

If you used a **new** domain for the destination, you're done — Caddy issues a TLS cert for it within ~30s and the site is live.

If you used the **same** domain as the old host, the migration ran against your old server's IP. To move traffic over, update the A record at your DNS provider to point at this server. Within DNS propagation time (usually 1–10 min), traffic shifts to the migrated copy and Caddy renews the cert under the new IP automatically. The old install on the source keeps serving the same domain until DNS catches up; you can shut it down on the source side once you're satisfied with the migration.

> **DNS heads-up on the done screen.** The wizard surfaces a callout if the destination domain isn't yet pointing here — Caddy can't get an HTTPS cert until DNS catches up. The Restore step also halts and tells you if DNS isn't in place yet.

## Common questions

**Will my site be down during the migration?** No — the source serves traffic the whole time. The migration only **copies**. You only have downtime at the moment you flip DNS (or never, if you're using a new domain on the new server).

**What about plugins / themes / custom code on WordPress?** Bundled. The migration captures the WordPress files (themes, plugins, uploads) AND the database in a consistent snapshot. After restore, the site behaves identically — same content, same URLs (or rewritten URLs if you picked a different domain).

**My database is bigger than the source's disk has free.** Won't work as-is — `mysqldump` needs scratch space on the source equal to the dump size, briefly. Free some space on the source first, or migrate via SQL dumps manually (download the dump → upload here via [Restore from a backup](/help/restore-from-a-backup)).

**Can I migrate from anything other than SSH/cPanel?** Today, no — the scan/probe logic only knows those two shells. For other panels (Plesk, DirectAdmin, etc.), the path is: SSH into the source manually, `tar` your webroot, `mysqldump` your database, package into a `.tar.gz` matching the backup-bundle layout, and import via [Restore from a backup](/help/restore-from-a-backup). If you need the exact bundle format spec to hand-craft one, ask via [Contact](/help/contact) and we'll share it.

**What if the migration fails halfway through?** The error screen offers a one-click **Clean now** button that removes any partially-written staging dirs under `/tmp/helm-restore/`. Source-side, nothing was modified. Re-running the wizard from the start is safe.

**Will it overwrite an existing site at this destination?** Only if you explicitly accept that in the Restore step. The default in the chat handoff is to install side-by-side — you'd pick "clone to new domain" (or, for databases, "clone alongside" with a suffix) if there's already a workload at the target domain.

**Is anything left on the source after migration?** No artifacts that we add. The wizard makes only **read** calls; the only thing that writes on the source is `mysqldump` (for WordPress + database migrations), which produces a file the wizard then immediately downloads and deletes. After the wizard disconnects, the source is in the same state as before.

**Secrets — are they re-used or rotated?** The DB password from `wp-config.php` is used to run `mysqldump` on the source, then thrown away. The restored site comes up with **fresh** database credentials generated by the chat handoff — no source-side credentials leak into the new install.

## What's NOT in scope here

- **Bundle-to-bundle restore** (you already have a `.tar.gz` exported from somewhere else) → see [Restore from a backup](/help/restore-from-a-backup) directly.
- **Server-to-server move** within Server Manager (both source + destination are Helm-managed) → see [Backups → Move to another server](/help/backups). That path is more efficient: no probe step, no manifest reconstruction.
- **Engine swap** (you're on nginx/Apache and want Caddy here) → see [Migrate to Caddy](/help/migrate-to-caddy). Different problem; that one rewrites configs in place, doesn't pull a bundle.
- **Email + DNS** → not in scope. Set those up separately via [Connect a domain](/help/connect-a-domain) and [Set up email for your domain](/help/set-up-email-for-your-domain).

## Reference

**Source paths the SSH scan checks:**

- `/var/www/*/` — common Nginx / Apache vhost root
- `/srv/www/*/` — Debian-style alternative
- `/home/*/public_html/` — cPanel-style document root
- `/home/*/domains/*/public_html/` — DirectAdmin-style document root
- WordPress: any of the above that contains a `wp-config.php`
- Databases: enumerated via `mysql -e "SHOW DATABASES"` using the SSH user's `~/.my.cnf` if present

**cPanel scan endpoints used:**

- `WebVhosts` (list vhost document roots)
- `MysqlFE/listdbs` (list databases)
- Per-vhost file-tree probe for `wp-config.php`

**On-disk staging during migration:**

- Source side: temporary mysqldump under `/tmp/` then immediately deleted
- This server: `/tmp/helm-restore/<id>/<bundle>.tar.gz` — same path the Restore wizard uses

**Bundle format:** Server Manager's own, and interchangeable with what the Backup tab produces — so a migrate-then-restore is byte-identical to a backup-then-restore (the difference is just where the bundle came from).

---

# Clean up this server

URL: https://servermanager.dev/help/clean-up-your-server
Category: Backups & recovery
Last updated: 2026-05-31

> A guided sweep that frees disk space, installs pending security updates, and (optionally) restarts long-running services. You pick what runs from a pre-flight checklist; safe steps run silently, risky ones pause for confirmation in chat; a reboot is offered (never forced) when the kernel update needs it.

A server that's been running for months collects clutter: package downloads it doesn't need anymore, old kernel images in `/boot`, stopped Docker containers, build caches, rotated log files, security updates waiting to be applied, and services that have slowly been leaking memory. None of this is wrong individually, but it stacks up until the disk fills, an update fails, or a site gets slower than it should be.

**Clean up this server** is one button that sweeps all of it in a single pass — and a chat conversation that confirms anything risky before it runs.

## What it actually does

Three categories, each runnable independently — tick what you want and leave the rest:

- **Free up disk space** — clear the package download cache, remove dependencies of removed packages, prune stopped Docker containers + dangling images, trim Docker build cache older than a week, vacuum systemd logs to the last 30 days, delete rotated log files (`.gz` / `.1` / `.old`) older than 30 days, and optionally remove old kernel images / disabled snap revisions.
- **Install security updates** — refresh the package list and install the newest version of every installed package (`apt update` + `apt upgrade`). If the update includes a new kernel, that becomes a "restart when convenient" follow-up rather than something that just happens to you.
- **Restart leaky services** — optional. Restarts your websites + databases + workers one by one (~10–30s downtime each) so the memory they've slowly accumulated over weeks gets reset. We sample response time before and after and only claim "faster" if there's actual evidence.

On top of those, a handful of read-only checks always run: disk hotspot report (which directories are eating your disk), failed services (anything systemd flagged), recent SSH logins (so you spot the unfamiliar ones), Fail2ban status, and a check for `/var/run/reboot-required` (the file the kernel update drops to signal "you need to restart").

## How to start it

Four entry points, same modal:

| Where you are | How |
|---|---|
| You want to start fresh | Type `/cleanup` in the chat composer and press Enter |
| You're browsing recipes | Open the [[recipe-palette]], find **Clean up this server**, click |
| The disk is starting to worry you | Open Server details → **Storage** tab → the **Clean up this server** CTA card at the top |
| You see "N updates available" on the Updates tab | Server details → **Updates** tab → the **Install these in a full cleanup** CTA card |

The Storage CTA is the most common path — you noticed the disk filling, you went to look at storage, and the cleanup button is sitting right there.

![Storage tab in Server details with the "Clean up this server" CTA card at the top — sage gradient background, broom icon, primary "Start cleanup" button on the right](/help/clean-up-your-server/01-storage-cta.svg)

## The pre-flight modal

Clicking any entry point opens the same checklist. The modal is doing two jobs at once: showing you exactly what's about to happen, and authorizing the safe steps so the chat doesn't waste your time asking again for things you already ticked.

![Pre-flight modal — eyebrow "SERVER CLEANUP", title "What we'll do on vps-test-amd", three sections (Package maintenance / Docker / Logs & system files) with checkboxes, risk dots, and per-row "What does this do?" disclosures; restart-workloads toggle at the bottom; always-run health-checks list; Cancel / Start cleanup buttons](/help/clean-up-your-server/02-modal-overview.svg)

Read the dots — they're how you know what to expect:

- **Sage (safe)** — runs immediately. No chat prompt, no confirmation, just a one-line "did it" status afterward.
- **Peach (confirm in chat)** — pauses for a yes/no in the conversation before running. Used for things like installing updates that might restart services.
- **Rose (confirm per item)** — lists each candidate (e.g. each orphan Docker volume) and asks one-by-one. The "are you really sure about *this specific one*" tier.

Each row also has a **"What does this do?"** disclosure with the exact shell command we'll run, so technical users can verify there's no surprise. Non-technical users can ignore it.

![Anatomy of a single row — checkbox, risk dot, label "Remove unneeded packages", short description, expanded "What does this do?" disclosure showing the helpBody paragraph + the literal `sudo apt autoremove -y` command](/help/clean-up-your-server/03-modal-row-help.svg)

The **Restart your workloads after cleanup** toggle at the bottom of the modal is its own decision. It restarts every running website / database / worker one at a time with ~10–30 seconds downtime each, measures response time before and after, and tells you the delta. Leave it off if downtime now is worse than slightly-leaky-memory; flip it on if you're already doing maintenance.

Your ticks are remembered per server — next time you open the modal on the same server, the same boxes are already ticked. New actions added by future updates auto-appear as ticked-by-default for actions that are safe (and unticked for the rest).

## What happens during the run

Click **Start cleanup** and the modal closes. From here, the conversation drives it:

1. **Snapshot** — one fast batch of read-only commands captures the "before" picture (disk free, RAM, log size, pending updates, failed services, biggest directories, recent logins, Fail2ban). The chat just says *"Snapshot taken, X GB free, N updates pending. Starting cleanup…"* — you don't see the wall of raw output.
2. **Safe steps auto-run.** `apt update`, `apt clean`, `journalctl --vacuum`, `docker system prune`, the rotated-log delete — these run silently, each emits a one-line status. No approval prompts (the modal authorized them).
3. **Caution steps ask.** Anything peach-dotted (`apt upgrade`, `docker system prune -a`, kernel removal, snap revisions) gets a one-line question in chat before running. You say *yes*, the action runs; you say *no*, it's skipped and the next action moves forward — the recipe doesn't halt on a single decline.

   ![Chat: Faro asks "Ready to install 14 security updates? Some services may restart briefly." with Yes / No reply chips inline](/help/clean-up-your-server/04-chat-confirm.svg)

4. **Per-item steps ask once per candidate.** If you ticked orphan-volume removal, Faro lists each dangling volume and asks one at a time, including a hint of which image used to mount it. *"No to all"* / *"stop"* / *"skip the rest"* short-circuits the remaining items.

   ![Chat: per-item orphan-volume prompt — "Remove orphan volume 'old-postgres-data'? (Used to be: postgres:14)" with Yes / Skip / Stop chips](/help/clean-up-your-server/05-orphan-per-item.svg)

5. **Optional workload restarts.** If you toggled it on, each running site/database gets restarted with response-time samples before and after.

If at any point the kernel updates and a reboot is needed to actually use the new kernel, the recipe **does not auto-reboot**. It surfaces a follow-up card you can act on later.

### When a reboot does happen

Anything that drops the SSH connection — `systemctl reboot`, restart of `sshd` / `dbus` / `networking` — triggers a fixed amber banner pinned at the top of the chat. The chat halts itself with a synthetic *"Reboot issued. Chat will go quiet for ~30–60s…"* message; sending while the banner is up is disabled.

![Amber rebooting banner pinned at the top of the chat — spinner + "Rebooting vps-test-amd · 22s elapsed" + "Reconnecting when it's back…" subline](/help/clean-up-your-server/06-rebooting-banner.svg)

Behind the scenes a probe is re-attempting SSH every couple of seconds using the credentials Server Manager cached when you connected. When the server answers, the banner flips green (*"Server is back · uptime 8s · resuming monitoring"*), auto-dismisses after 5 seconds, and the composer re-enables. No browser refresh needed.

## The summary and "What you need to do"

When the run finishes, Faro posts a closing message with the wins quantified up front and a table of changed metrics.

![Summary message — "Reclaimed 6.2 GB and patched 14 security updates" headline, "What changed" table with Before / After / Change / Why-it-matters columns; one row per metric that actually changed](/help/clean-up-your-server/07-summary.svg)

The table only includes rows where the underlying action actually ran AND the change is non-zero — so if you skipped Docker, you don't get a Docker row. The "Why it matters" column is plain English ("Room for databases and uploads", "Vulnerabilities patched", "Less competition for CPU"), not jargon.

If a workload restart produced a measurable improvement, you'll also see a per-workload **Workload response time** table — and only then. The recipe won't claim *"your site is faster"* on noise.

### Follow-up action cards

Anything the cleanup couldn't finish on its own — a pending reboot, services running outdated library code, workloads you declined to restart, orphan volumes you said *no* to — renders as a **What you need to do** section with numbered cards. Each card offers two ways to handle it:

![Phase 6 "What you need to do" — numbered cards each with "Option A — ask me here" (callout with quick-reply text) + "Option B — through DigitalOcean" (numbered console-click steps) + a one-line "Trade-off" comparing them; sticky follow-ups chip strip pinned above the composer](/help/clean-up-your-server/08-action-cards.svg)

- **Option A — ask me here** — a one-click chat path. The callout's reply text shows up as a clickable chip above the composer so you don't have to scroll back up.
- **Option B — through your provider console** — provider-specific click-by-click steps for the same thing (e.g. *"DigitalOcean → Droplet → Power → Reboot"*). Useful if you'd rather do it deliberately yourself.
- **Trade-off** — one sentence per card explaining when each path makes more sense.

The chip strip pinned above the composer mirrors these cards as ☐ pending follow-ups. Click a chip → its reply text fills the composer → press Send. As Faro works on it, the chip flips ⏳ in-progress; when it's done, ✓ struck-through and auto-clears after 5 seconds. Each chip also has a small **×** dismiss button if you've decided to ignore that follow-up — that hide persists for the rest of the browser session.

## Common edge cases

**"No orphan volumes found"** — the per-item action just says so and skips. Not an error; the list was empty.

**Reboot-required was already set before the cleanup** — common when someone else (or a previous run) installed updates without rebooting yet. The recipe checks `/var/run/reboot-required` both at snapshot time and after `apt upgrade`. If it was already there, the "Restart the server" follow-up card appears even when this cleanup didn't install anything new — it's still the right action.

**My sites are behind Traefik / nginx / Apache** — the recipe doesn't touch your web server's config. It restarts the underlying containers (or systemd units) for sites it can identify from the inventory; the proxy keeps running normally. If a workload's reverse-proxy is sitting in front of containers Server Manager doesn't know about (an unmanaged custom setup), it's silently skipped — no probe URL means no measurable restart.

**Snap isn't installed on this server** — the "Remove disabled snap revisions" step just says *"Snap isn't installed — skipping."* and continues. Ticking it on a non-snap server is harmless.

**`apt autoremove` would remove something I want to keep** — every `autoremove` step is preceded by a dry-run that prints *"Would remove: pkg-a, pkg-b, …"*. If the list looks wrong, say *no* — the real run only happens after you confirm.

**I said yes to a step and want to back out mid-stream** — pressing **Esc** halts the agent. Commands already running aren't interrupted (they finish), but the loop exits. You can ask Faro to revisit anything skipped later.

**The chat looks stuck during a service restart** — anything restarting `sshd`, `dbus`, or the network stack triggers the same banner the reboot does (short-horizon variant). If the banner shows, the chat is intentionally paused; if it doesn't show and chat truly seems frozen, see [Recover when SSH stops working](/help/recover-when-ssh-stops-working).

## The final report — what to look for

The summary message has a deliberately consistent shape so you can scan it fast:

1. **Headline** — one bold line: *"🧹 Reclaimed 6.2 GB and patched 14 security updates."* That's the win. The number that wasn't moved isn't mentioned.
2. **One-line elaboration** — the biggest specific improvement in plain English (most disk freed, security patched, fewer failing services — whichever was the biggest).
3. **"What changed" table** — Before / After / Change / Why-it-matters, one row per metric that actually moved. Possible rows: free disk space (GB and %), free inodes (%), Docker disk usage, system-log size, pending security updates, failed services, available memory, background load.
4. **"Workload response time" table** (only if you opted into restarts AND there was a measurable win) — per-workload before / after / change.
5. **"What you need to do" section** (only if there are follow-ups) — the numbered cards covered above.
6. **"Skipped: …" footer** (only if you declined steps) — the list of action labels so you can re-run later and revisit.

If the server was already tidy and nothing needed doing, Faro closes with *"Your server's already tidy. Nothing to do."* — no fabricated summary table, no padded report.

## Reference

**What gets pre-authorized vs what still asks**

| Tier | Examples | Behavior |
|---|---|---|
| Safe | `apt update`, `apt clean`, `docker system prune -f`, `journalctl --vacuum-time=30d`, `find /var/log … -delete` | Pre-authorized by your modal ticks — auto-runs, one-line status |
| Caution | `apt upgrade`, `docker system prune -a`, kernel removal, snap revision removal | Pauses for chat yes/no |
| Per-item | Orphan volume removal | Asks one-by-one; "stop"/"skip the rest" short-circuits |
| Read-only health checks | `df`, `free`, `last`, `fail2ban-client status`, disk hotspots | Always run (regardless of ticks) |

**Storage of your ticks** — your selection (and the workload-restart toggle) is saved in your browser's `localStorage` keyed by server hostname. Clearing browser data resets the ticks back to the default selection.

**How follow-ups + reboot bypass work** — when the cleanup recipe is active, the recipe's read-only snapshot scripts and any of the safe-tier commands you ticked in the modal bypass the destructive-command classifier — that's what avoids the duplicate "Approve this command?" prompts. Caution + per-item tiers always re-ask. The bypass is content-aware (every command segment is validated against a read-only allow-list) and forbidden patterns like `rm -rf /` always block regardless.

---

# Recover when SSH stops working

URL: https://servermanager.dev/help/recover-when-ssh-stops-working
Category: Backups & recovery
Last updated: 2026-05-26

> SSH is the only way Server Manager talks to your server. If it stops working, Server Manager goes dark too — but you can still get back in. Triage by symptom, then recover via your provider's web console.

Server Manager talks to your server **only over SSH**. If SSH stops working, Server Manager can't help — it has no other channel into the machine. But your server is fine, and you have other ways to get back in. This article triages the most common symptoms and walks the recovery paths.

> **Read this before panicking.** None of the standard SSH-broken scenarios mean your data is gone. The server itself is still running; you just can't log into it from where you usually do. Worst case, you reinstall SSH access via your cloud provider's web console — same machine, same disks, same files.

## Step 1 — Read the error in plain English

If you tried to connect through Server Manager and it failed, the **Connect** dialog already shows a plain-English diagnosis of what's likely wrong:

![ConnectModal showing a friendly error message under the credentials form](/help/recover-when-ssh-stops-working/01-connect-error.svg)

That message names the most likely cause. The table below maps the message Server Manager shows you to where to look next.

| If the error says… | Likely cause | What to try first |
|---|---|---|
| **"The server didn't respond within 10 seconds"** *(timeout)* | Cloud firewall blocking port 22, wrong IP, or server powered off | Open your cloud provider's web dashboard; check "Security List" / "Security Groups" / "Firewall Rules" for port 22; verify the IP matches |
| **"The server is reachable but nothing is listening on port 22"** *(connection refused)* | SSH daemon not running, or it's on a different port | Provider console; check that `sshd` is running; look for a non-standard port in your `/etc/ssh/sshd_config` |
| **"The server refused the credentials"** *(permission denied)* | Wrong username, wrong password, or the private key isn't authorized | Try a different username (`root` / `ubuntu` / `debian` / `centos` are common defaults); re-check the password from your provider's signup email; verify the key in `~/.ssh/authorized_keys` |
| **"The hostname couldn't be looked up"** *(DNS)* | Typo in the host field, or the domain stopped pointing at the IP | Use the raw IP instead of the domain name; check the A record at your DNS provider |
| **"No network route to that host"** | Network outage, corporate VPN interference, or the server is gone | Try from a different network (mobile hotspot); verify the server still exists in your provider's dashboard |
| **"SSH handshake failed"** | Very old server with incompatible crypto, or unusual hardening | Server is reachable — needs a config fix on the server side; use the provider's web console to undo recent `/etc/ssh/sshd_config` changes |
| **"Wrong encryption passphrase for this saved server"** | You're using the wrong passphrase for a saved-server entry | This is the passphrase you set when you first **saved** the credentials, not your SSH key passphrase. Re-enter manually if you can't remember it |

## Step 2 — Recover via your provider's web console

If the diagnosis above points at "something is broken on the server itself" (most of the rows in the table), the way back in is your **cloud provider's web console** — a browser-based terminal that bypasses SSH entirely. Every major provider has one, just with different names:

![Generic cloud-console layout — VNC/Serial console, SSH keys, snapshots/restore tabs](/help/recover-when-ssh-stops-working/02-provider-console.svg)

| Provider | Console name | Where to find it |
|---|---|---|
| **DigitalOcean** | Recovery Console / Web Console | Droplets list → your droplet → **Recovery** tab → **Boot from Recovery ISO** or **Web Console** |
| **Hetzner Cloud** | Console | Servers list → your server → **Console** tab (top right) |
| **AWS EC2** | EC2 Serial Console | EC2 console → instance → **Actions → Monitor and troubleshoot → EC2 Serial Console** (or Session Manager if installed) |
| **Oracle Cloud (OCI)** | Cloud Shell / Console Connection | Instance details → **Console connection** → Launch Cloud Shell connection |
| **Google Cloud (GCP)** | Serial Console / SSH-in-browser | Compute Engine → instance → **SSH** dropdown → **Open in browser window** |
| **Vultr / Linode** | Web Console | Instance → **Glish** (Vultr) / **Lish** (Linode) console |
| **Bare metal / homelab** | IPMI / iLO / iDRAC | Whatever out-of-band management your hardware has — usually a web UI on a separate IP |

Once you're in via the web console, you have full shell access **without going through SSH** — so you can fix whatever's blocking SSH and then go back to Server Manager.

> **The web console asks for the server's local password.** That's the OS-level password for your user on the server itself — usually the one your provider emailed you, or the password you set during initial setup. It's **not** your Server Manager passphrase or your cloud-provider account password. If you never set one and only used a key, run `passwd <user>` via your provider's metadata service (DigitalOcean has "Reset root password" → emails a new one; AWS has password reset via Systems Manager).

## Step 3 — Fix the most common causes

Once you're in via the web console, run through these:

### "I edited sshd_config and locked myself out"

The classic. You changed something in `/etc/ssh/sshd_config`, restarted `sshd`, and now you can't get back in. Recovery:

```bash
sudo sshd -t                                  # syntax-check current config
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.broken
sudo nano /etc/ssh/sshd_config                # undo the offending change
sudo sshd -t                                  # syntax-check again
sudo systemctl restart sshd                   # restart cleanly
```

Common things that lock people out:

- `PermitRootLogin no` while they're connecting as root → either re-allow root, or create a non-root sudo user first
- `PasswordAuthentication no` before adding their key to `~/.ssh/authorized_keys`
- `Port 2222` (or other non-default) — the server is still up, just listening elsewhere; reconnect to the new port
- `AllowUsers alice` that excludes the user you're actually logging in as

### "fail2ban banned my IP"

If you typed the wrong password 3–5 times in a row, `fail2ban` (or `sshguard`) often blocks your IP for 10 minutes to an hour. To check + unban:

```bash
sudo fail2ban-client status sshd               # see banned IPs
sudo fail2ban-client unban <YOUR-IP>           # unban yours
```

Your current IP is what `curl ifconfig.me` shows from your home connection (not the server). To prevent next time, add yourself to `/etc/fail2ban/jail.local` under `ignoreip = ` once you're back in.

### "ufw / iptables locked me out"

You enabled the firewall without explicitly allowing port 22:

```bash
sudo ufw status                                # see current rules
sudo ufw allow 22/tcp                          # explicitly allow SSH
sudo ufw reload
```

For `iptables` directly:

```bash
sudo iptables -L INPUT -n -v                   # see what's blocking
sudo iptables -I INPUT -p tcp --dport 22 -j ACCEPT
sudo netfilter-persistent save                 # save (Debian/Ubuntu)
```

### "I removed my own public key from authorized_keys"

If you accidentally cleaned out `~/.ssh/authorized_keys` (or `chmod`'d it wrong), re-add via the web console:

```bash
mkdir -p ~/.ssh && chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys                    # paste your public key, save
chmod 600 ~/.ssh/authorized_keys
```

The public key is the **short** one-line file usually at `~/.ssh/id_ed25519.pub` (or `id_rsa.pub`) on your laptop. Paste the whole line including the `ssh-ed25519 …` prefix and your `user@host` suffix.

If you don't have the original public key anymore, generate a new key pair on your laptop:

```bash
ssh-keygen -t ed25519 -C "you@laptop"          # creates ~/.ssh/id_ed25519 + .pub
cat ~/.ssh/id_ed25519.pub                       # paste this into authorized_keys
```

### "My provider's firewall changed by itself"

It didn't change by itself, but providers do periodically tweak default policies. Open your provider's **Security List / Network Security Group / Firewall Rules** page and verify that port 22 (or whatever port your SSH listens on) is still allowed from the public internet (`0.0.0.0/0`) or from your specific IP range.

![Generic cloud-firewall rules table with a port-22 entry highlighted](/help/recover-when-ssh-stops-working/03-cloud-firewall.svg)

If your IP changed (home ISP, moved cities, switched to mobile), and you'd narrowed the firewall to your old IP, the new IP is blocked. Widen the rule back to `0.0.0.0/0` temporarily, get in, then re-narrow if you want to.

## Step 4 — Reconnect from Server Manager

Once SSH works from your terminal (test with `ssh -v <user>@<host>`), come back to Server Manager and click **Connect server**. Re-enter the credentials and you're back where you were. The chat history from the previous session is gone (SSH sessions are RAM-only by design), but your sites and services on the server are untouched.

If you have the server saved (with a Server Manager encryption passphrase), pick it from the dropdown instead of re-entering.

## Last-resort options

If you can't get in even via the provider's web console, you still have options:

- **Snapshot rollback** — if you took a snapshot before things went wrong (and most providers offer them, sometimes automatic), restore to that snapshot. You lose changes since then but you get a known-good system back.
- **Rebuild from a backup** — if you have a Server Manager `.tar.gz` bundle (see [Backups](/help/backups)), spin up a fresh server, install Server Manager on it, and use the [Restore from a backup](/help/restore-from-a-backup) wizard. Same site, new server.
- **Detach disk, mount on another VM** — advanced, but works on most providers. Stop the broken VM, detach its boot disk, attach to a working VM as a secondary disk, mount, edit the files that were broken (sshd_config, authorized_keys), unmount, reattach to the original VM, restart. Your provider's docs have step-by-step.

## Prevention — before you change anything SSH-related

A short before-you-edit-sshd_config checklist that's saved a lot of people a 2 AM panic:

1. **Open a second SSH session** before editing — that way if you lock yourself out of the new session, the old one stays alive and you can roll back.
2. **`sudo sshd -t`** before restarting — catches most syntax errors.
3. **Test the new config without making it permanent.** `sudo /usr/sbin/sshd -D -p 2222 -f /etc/ssh/sshd_config.new` runs a **test** sshd on port 2222 against a **test** config. SSH in via `-p 2222` to verify; if it works, then swap the config and restart for real.
4. **Have your provider's web-console access ready** **before** you start. Open the tab, log in, confirm the console works, then start editing.
5. **Don't run `ufw enable` over SSH** without `sudo ufw allow 22/tcp` first.

For changes Server Manager makes via the chat: Faro pauses for your approval on every command, and won't propose anything that would break SSH (no `sshd_config` edits, no `ufw enable` without an explicit allow rule). It's the manual `nano /etc/ssh/sshd_config` sessions that catch people out.

## Common questions

**Did I lose anything?** No — your files, your sites, your databases are exactly as you left them. SSH is just the channel; the server itself is untouched.

**Is the chat history gone?** Yes. SSH sessions are RAM-only — when the session ends (your end or the server end), the chat resets. The work the chat did on the server stays, though: deployed sites, edited configs, installed services all persist normally.

**Can I avoid this entirely?** Mostly. The Server Manager–driven path is safer (every approval gate; no `sshd_config` edits). The risk comes from manual SSH sessions where you edit the config yourself. If you do everything through Server Manager, the lockout scenario almost never happens.

**My server is on a provider not listed in the table above.** The principle is universal: most providers have **some** form of out-of-band console (VNC, serial, web shell). Search your provider's docs for "console" or "rescue mode". If you have physical access (homelab), a monitor + keyboard does the same job.

**Server Manager says "Connection refused" but I just used SSH from my terminal 5 minutes ago.** Either `sshd` crashed (check via your provider's console: `systemctl status sshd`), or fail2ban banned the Server Manager VPS's IP (different from your home IP — Server Manager runs in its own datacenter and your server can see **that** IP as the source). Whitelist Server Manager's egress IP in fail2ban's `ignoreip = `, or relax the `findtime`/`maxretry` settings.

## What's NOT in scope here

- **Account-level recovery** (forgot your Server Manager login, forgot your cloud provider account password) — those are out-of-band: provider's account-recovery flow + Server Manager's password reset on the sign-in page.
- **Encrypted-disk rescue** (LUKS-encrypted root that won't unlock) — provider-specific; check your provider's docs for "rescue mode" or "single-user boot".
- **Permanently lost SSH key with no other access method** — at that point a rebuild from backup is the practical path. See [Restore from a backup](/help/restore-from-a-backup) and [Backups](/help/backups) for the full picture.

---

# Install WordPress

URL: https://servermanager.dev/help/install-wordpress
Category: Deploy & manage sites and apps
Last updated: 2026-05-26

> One click + one question (the domain), then approve the install commands. WordPress + MariaDB in Docker, fronted by Caddy with auto-HTTPS, ready in 2–3 minutes. WordPress's own first-run wizard handles site title + admin account.

You want WordPress running at your domain (or just at your server's IP), with HTTPS, behind a proper reverse proxy, on Docker so it doesn't tangle with anything else. Server Manager's **Install WordPress** recipe does all of that in ~2–3 minutes. You answer one question (which domain), then approve each command as the chat runs it.

## What you'll end up with

- A **WordPress 6** site running in a Docker container, talking to a **MariaDB 11** container on a private network.
- **Caddy** (already running on the host) reverse-proxying your domain → the container, with auto-issued **Let's Encrypt** HTTPS.
- The site's secrets (database password, the 8 WordPress salts) auto-generated and stored in `/opt/wordpress/.env`, mode `600`, root-owned. Nothing in plaintext anywhere else.
- A **workload card** on your home screen with the WordPress logo. Click `manage →` to get the per-recipe tabs: **WP-Admin · Plugins · Themes · Maintenance · Status · Logs · Controls · Backup**.

## Prerequisite — a domain (optional but recommended)

WordPress works fine at a raw IP for testing, but for a real site you want a domain with HTTPS. If you haven't already, run [Connect a domain](/help/connect-a-domain) first — point `mysite.com` at this server's IP and confirm DNS has propagated. The install wizard works with either, but DNS-then-install is the smoother path: Caddy can issue a TLS certificate immediately when the domain is already pointing here.

If you only have an IP for now, that's OK — you can attach a domain later without reinstalling: top bar → [[set-up-menu|**Actions**]] → **Point a domain here**. (Or use the **Connect a domain** button that shows up in the bottom action bar when you click a card that doesn't yet have a domain — same wizard, just pre-targeted to that workload.)

## The walk-through

### 1. Open the recipe palette

Press `/` anywhere in chat to open the palette, OR click **Browse all actions** below the chat, OR click **Actions** in the top bar and choose **WordPress**.

![Recipe palette with "Install WordPress" curated tile highlighted](/help/install-wordpress/01-palette.svg)

The palette has WordPress as a curated **Start here** tile at the top, and also under the **Web apps** category lower down. Either entry triggers the same install flow.

> **Note for non-Caddy servers.** If your server already runs nginx, Apache, or Traefik instead of Caddy, the WordPress recipe is "gated" — the tile shows a **💬 via chat** badge. Clicking it still works; Faro will set up WordPress natively for your engine instead of bringing in Caddy. See [Why Caddy?](/help/why-caddy) and [Migrate to Caddy](/help/migrate-to-caddy) for context.

### 2. Faro asks the domain

After you click the tile, the chat takes over. Faro's first message is a quick check ("any existing wordpress here?", "ports 80/443 free?", etc.) plus one short question:

![Chat showing Faro's question: "What domain should this WordPress site serve at?"](/help/install-wordpress/02-chat-asks-domain.svg)

Type the domain (e.g. `mysite.com`) — or `skip` if you want to test at the raw IP first. Then Send.

### 3. Approve each command as it runs

Faro pauses for your approval on every command that changes state. The first batch is usually:

- (only if missing) `apt install docker.io docker-compose-plugin` — install Docker.
- `mkdir -p /opt/wordpress` — install dir.
- `openssl rand -base64 32` (DB password) and `openssl rand -base64 48` × 8 (the WordPress salts).
- Write `/opt/wordpress/docker-compose.yml` (pinned `wordpress:6` + `mariadb:11`, named volumes, internal network, port published to `127.0.0.1:<random>` — never `0.0.0.0`).
- Write `/opt/wordpress/.env` with all the secrets, `chmod 600`.
- `docker compose up -d` — start the containers.
- Caddyfile block for your domain → `https://mysite.com { reverse_proxy 127.0.0.1:<port> }` → `caddy reload`.

![Chat showing a "docker compose up -d" command awaiting Approve / Skip / Cancel](/help/install-wordpress/03-chat-approval.svg)

Each command's exact text is visible before you approve. You can **Approve**, **Skip** (rare — usually breaks the install), or **Cancel** (leaves nothing running but also nothing on disk that we can't clean up).

> **Why so many approvals?** Server Manager doesn't run anything destructive without your explicit OK. Even for a routine install. It's a deliberate friction. After the first install you'll know what to expect; later installs feel quicker because the approval rhythm is familiar.

Total install time including approvals: usually 2–3 minutes. Most of that is the Docker pull (~500 MB of WordPress + MariaDB images on first install).

### 4. Faro hands you the URL

When the containers are up and Caddy is reloaded, Faro gives you the URL:

![Chat showing the install-complete message with the WordPress URL highlighted](/help/install-wordpress/04-chat-done.svg)

It also asks a proactive follow-up: **"Want me to set up nightly backups of the database?"** Pick yes or no — you can always set them up later (see [Backups](/help/backups)).

### 5. Complete WordPress's own first-run wizard

Click the URL. WordPress's **own** install wizard opens — Server Manager's setup is done; from here you're talking to WordPress directly.

![WordPress 5-minute install screen — Site title, Username, Password, Your Email, then Install WordPress](/help/install-wordpress/05-wp-first-run.svg)

Set the **site title**, choose an **admin username** (not "admin" — pick something less guessable), set a **strong admin password** (or let WP generate one and save it in your password manager), enter the **admin email**. Click **Install WordPress**.

> **WordPress's admin credentials are NOT the same as your Server Manager login.** They live inside WordPress's database, separate from Server Manager. Pick a strong password and save it in a password manager — there's no way to recover a forgotten WP admin password from Server Manager's side without database surgery (the chat can do it; see "Common questions" below).

You're done. Log into `/wp-admin/`, write your first post, install a theme, install plugins — same as any WordPress site.

### 6. Back in Server Manager

A workload card now shows on your home screen with the WordPress glyph + your site's title:

![Workload card on home screen — WordPress site with "OK · RUNNING" status](/help/install-wordpress/06-workload-card.svg)

Click `manage →` to open the [[service-panel]] with the per-recipe tabs:

- **WP-Admin** — a one-click link to `https://your-domain/wp-admin/`.
- **Plugins** — list of installed plugins with one-click Activate / Deactivate / Update / Delete. (Adding new plugins still happens through WP-Admin — that's where the plugin search index lives.)
- **Themes** — same as Plugins but for themes.
- **Maintenance** — three things in one tab: **Flush WordPress cache** (force-rebuild on the next visit), **Search & replace** the database (typically for changing the site's URL — wp-cli-correct so serialized PHP data doesn't get corrupted; defaults to a safe dry-run preview), and **Update WordPress core** (with a Backup / Clone reminder before changing anything).
- **Status / Logs / Controls / Backup** — the universal tabs (every workload kind has these).

![ServicePanel WP-Admin tab — Destination URL + "Open WordPress admin →" button](/help/install-wordpress/07-svc-wpadmin.svg)

![ServicePanel Plugins tab — list of plugins with Activate/Deactivate buttons](/help/install-wordpress/08-svc-plugins.svg)

## What got installed, exactly

For reference (you don't need to touch any of this — the service panel handles everything):

| Where | What |
|---|---|
| `/opt/wordpress/docker-compose.yml` | Container definitions (WordPress 6 + MariaDB 11) |
| `/opt/wordpress/.env` | Secrets (DB password + 8 WP salts) — `chmod 600`, root-owned |
| Docker named volume `wp_data` | WordPress wp-content (uploads, plugins, themes) |
| Docker named volume `db_data` | MariaDB data files |
| Docker network `wordpress_default` | Internal network for the two containers to talk |
| `/etc/caddy/Caddyfile` | The reverse-proxy block for your domain (a `caddy reload` away from active) |
| Caddy data dir | Auto-issued Let's Encrypt cert for your domain |

The WordPress container publishes its port on `127.0.0.1:<random in 38000–39999>` — **only** to localhost. Caddy reaches it via that local port; the public internet only ever sees Caddy on 80/443. So even if WordPress had a vulnerability, the container itself isn't directly exposed.

## Common questions

**Where do I get plugins?** Through WordPress's **WP-Admin → Plugins → Add New** search, like any normal WP site. Once installed, the **Plugins** tab in Server Manager's service panel shows them with one-click activate/deactivate/update/delete.

**Can I edit `wp-config.php`?** It's inside the container, regenerated on `docker compose up`. Custom changes go in `/opt/wordpress/.env` (the secrets + things WordPress reads via env vars) or in `/opt/wordpress/docker-compose.yml` (the container definition itself). Adding a custom env var: ask Faro — "add WP_DEBUG=true to the wordpress container" — and approve the change.

**I lost the admin password.** Don't use WordPress's "Lost your password?" link unless you've set up [Send email from your domain](/help/set-up-email-for-your-domain) (otherwise the email goes nowhere). Instead ask Faro: *"reset the WordPress admin password."* It runs a safe in-DB rotation pattern (no plaintext password ever printed) and tells you the new password once.

**I lost the admin username too.** Ask Faro: *"who's the WordPress admin?"* — it lists the admin-role accounts from the database.

**Can I run multiple WordPress sites on the same server?** Yes. Just run the **Install WordPress** recipe again with a different domain — Faro will detect the existing `/opt/wordpress/` install (its pre-check greps `docker ps` for wordpress) and propose a non-colliding install dir (typically `/opt/wordpress-<short-name>/`) so the new containers, volumes, and Caddy block don't clash with the original. Caddy multiplexes by domain. See [Multiple sites on the same server](/help/multiple-sites-on-the-same-server) for the long version.

**WordPress is slow / running out of memory.** Each install needs ~256–512 MB RAM at idle (WordPress + MariaDB combined). On a 1 GB server you can run one comfortably; on 2 GB, two; etc. The home screen's workload card shows the current RAM use — if you see it climbing toward the server's total, it's time to scale up the server or move heavy sites to bigger instances. If WP-Admin specifically reports "Allowed memory size exhausted" mid-action, ask Faro in chat to bump PHP's `memory_limit` — it'll edit the right config file inside the container and restart.

**Can I migrate an existing WordPress site here?** Yes — that's the [Migrate an existing site here](/help/migrate-an-existing-site) wizard. It copies your site's files + DB off your old host and installs it here via the same recipe.

**Can I get HTTPS without a domain?** Not from Let's Encrypt (they require a real domain). The site will serve over plain HTTP at `http://<your-ip>` until you point a domain at the server, then HTTPS auto-issues within ~30 seconds.

## What's NOT in scope here

- **WordPress multisite (network)** — the recipe installs a single-site WP. Multisite is enabled inside WordPress itself, after install, via `wp-config.php` changes. Ask Faro to enable it; the change is in the env file and a flag in WP's database.
- **Custom WordPress themes/plugins shipped with your code** — the recipe installs a stock WP. If you have a custom theme + plugins as code, deploy via [Migrate an existing site here](/help/migrate-an-existing-site) (from your dev environment) or as a backup bundle via [Restore from a backup](/help/restore-from-a-backup).
- **Hardening / WordPress security configuration** — the recipe gives you reasonable defaults (random secrets, port not exposed publicly, env-var secrets). Things like 2FA, Wordfence, malware scans — install those as plugins inside WordPress.
- **Sending mail FROM WordPress** — the WP-Mail or contact-form plugins need an SMTP/API endpoint. Run [Send email from your domain](/help/set-up-email-for-your-domain) (Resend) and plug those credentials into a WP SMTP plugin.

## Reference

**Default install paths:**

- Install dir: `/opt/wordpress/` (or `/opt/wordpress-<suffix>/` if multiple)
- Compose file: `/opt/wordpress/docker-compose.yml`
- Env / secrets: `/opt/wordpress/.env`
- Caddy block: `/etc/caddy/Caddyfile` — search for your domain's `reverse_proxy` line

**Container names** follow Compose's default `<project>-<service>-<n>` pattern — typically `wordpress-wordpress-1` (web) and `wordpress-db-1` (database) on modern Compose v2; older v1 uses `_` separators (`wordpress_wordpress_1`). Run `docker ps` to confirm exactly what's running on your server.

**Internal port range:** `38000–39999` — the recipe picks a free one randomly and publishes to `127.0.0.1`. Only Caddy on the host reaches it; the public internet doesn't.

**WordPress + MariaDB versions:** pinned to `wordpress:6` and `mariadb:11` major tags. Bug-fix updates pull on `docker compose pull`. Major-version upgrades (WP 6 → 7, MariaDB 11 → 12) are a separate ask in chat — they sometimes need DB migrations.

**To uninstall:** the home-screen card's `manage →` panel has a **Delete** action under **Controls**. It stops the containers, optionally wipes the named volumes (so you lose your data) or keeps them (so you can re-install later), and removes the Caddy block. There's also a one-click in the bottom action bar (click the card → Delete).

---

# Understand the Health view

URL: https://servermanager.dev/help/understand-the-health-view
Category: Backups & recovery
Last updated: 2026-05-27

> "What should I worry about right now?" — Server Manager's at-a-glance check for the server. Three urgency buckets (Act now / This week / All good), each finding has a one-click action that drops a remediation prompt into the chat.

The **Health** view is the answer to one question: *"What should I worry about right now?"* It's a sibling to the home-screen Overview, accessed via the **View** menu at the top of the right-hand column. Findings are grouped into three urgency buckets, and each one has a one-click action that drops a remediation prompt into the chat.

## Where it lives

Look at the right-hand column (the one with your workload cards). At the very top of that column there's a **View: Overview** button. Click it; the menu has two items: **Overview** (the default, with your workload cards) and **Health**.

![View menu open with Health item highlighted; both items have one-line descriptions](/help/understand-the-health-view/01-view-menu.svg)

A small colored pip on the **View** button signals there's something to look at:

- **Red pip** — at least one **Act now** finding. Switch to Health to see what.
- **Amber pip** — only **This week** findings. Not urgent but worth a look soon.
- **No pip** — everything's green (or there's nothing to report yet).

The pip is suppressed while you're already on the Health view (would be self-evident there).

## The three buckets

Once you're on the Health view, the top of the page shows three count pills:

![Three pills at top: Act now (red, 1) / This week (amber, 3) / All good (green, 2)](/help/understand-the-health-view/02-three-pills.svg)

| Pill | Color | Meaning |
|---|---|---|
| **Act now** | Red | Something is broken or actively degrading. Address today. |
| **This week** | Amber | Not on fire, but ignoring it for a month or two will probably cause pain. |
| **All good** | Green | Positive confirmations — things that are *correctly* set up. Useful for sanity-checking after changes. |

Below the pills, the same findings list as a stream, grouped by bucket, ordered urgency-first.

## What gets surfaced

The findings are derived from Server Manager's regular probes of your server (the inventory + metrics that drive the rest of the UI). Each check has a fixed threshold; you'll see one finding per actual condition. The current set:

### Red — Act now

- **Disk is N% full** (≥ 90%) — when the disk fills up, services start failing. The action ("Investigate disk") asks Faro to find the biggest directories and any Docker waste, then propose a safe cleanup plan.

### Amber — This week

- **Disk is N% full** (≥ 80%, < 90%) — not critical yet, but climbing. Same investigate action as the red variant.
- **RAM is N% used** (≥ 90%) — note that Linux uses free RAM for cache, so high usage isn't always a problem. The action surfaces top memory-consuming processes and any recent OOM kills so you can tell.
- **N container(s) restarting** — usually means crashing on startup. The action pulls the recent logs of the affected containers + asks Faro to explain.
- **N container(s) stopped** — silent stops (vs. running). The action checks exit codes + last logs.
- **N site(s) served over HTTP without TLS** — a Caddy/nginx block has a domain but no HTTPS. Almost always a config mistake since Let's Encrypt is free + automatic. The action ("Add HTTPS") asks Faro to update the proxy config and verify the cert issues.
- **N container image(s) have updates available** — aggregated across all images (so you don't get N findings). Image updates often include security patches; the action ("Review updates" / "Update image") asks Faro to check changelogs and pull the new image with your approval.

### Green — All good

- **N website(s) configured behind Caddy/Nginx** — positive confirmation that your proxy + sites are wired up.
- **N system service(s) running normally** — positive confirmation that the underlying systemd-managed processes are alive.

## A finding row, in detail

Each row in a section looks like this:

![Finding row with severity dot, message, "tell me more" link, and Action button on the right](/help/understand-the-health-view/03-finding-row.svg)

Parts:

- **Severity dot** on the left — matches the bucket color.
- **Message** — one short sentence naming the problem with the specific numbers (e.g. "Disk is 91% full — only 4 GB free of 47 GB").
- **"Tell me more"** — expands an explanation of what the finding actually means and why it matters. Click again to collapse.
- **Action button** on the right — drops a pre-written prompt into the chat, ready to send. Faro takes over from there (with the usual approval gates on anything destructive).
- **"✓ ok"** instead of a button — for green findings, there's nothing to do. The pill is the affordance.

### Example: clicking the action

The action button doesn't *immediately* run anything — it composes a prompt and puts it in the chat composer, so you can read what's about to be asked, edit it if you want, and hit Send. Then Faro picks it up.

![Health action button click → chat composer pre-filled with the remediation prompt](/help/understand-the-health-view/04-action-to-chat.svg)

For destructive remediations (cleanup, container recreate, etc.), Faro still pauses for explicit approval on every command. The Health-view action button is a shortcut to *starting the conversation*, not a one-click execute.

## When the Health view says "nothing to report yet"

You'll see this if you just connected and the first metrics + inventory polls haven't returned yet (the first inventory poll runs immediately on session start; subsequent polls are every 15 s). Until the data lands, the view is empty.

If you've been connected for a while and still see "nothing to report yet", a poll likely failed silently. Refresh the page; if the issue persists, disconnect and reconnect to reset the polling loop.

## What the Health view DOESN'T cover

Important to know what's outside the scope:

- **App-level health inside a container.** "Is my WordPress responding to login attempts?" "Is my database query slow?" Those are inside the application — the Health view sees the container is running but doesn't know whether the app inside is happy. For app-level signals, use the workload's service-panel **Logs** tab or ask Faro directly.
- **Outbound connectivity / external dependencies.** "Is Stripe's API up?" "Is my third-party SMTP working?" Server Manager doesn't probe external endpoints from your server.
- **DNS reachability of your domains.** The view checks whether sites have TLS, not whether they actually resolve to this server. If you set up a new domain and DNS hasn't propagated, the Health view won't notice — it just sees the proxy config locally.
- **Security posture.** No CVE scanning, no log anomaly detection, no fail2ban status. Things like image updates (a security signal) are surfaced, but a full security view is a separate project. (See [Will Server Manager break my server?](/help/will-server-manager-break-my-server) for what's covered in terms of security defaults.)

## Common questions

**Should I always have zero red + zero amber findings?** Not necessarily. Some amber findings (like 1–2 stopped containers) might be intentional (a paused dev environment, a `docker-compose down` you did on purpose). The view shows you the state — you decide whether it warrants action. The pill colors are heuristics, not orders.

**Can I dismiss / snooze a finding?** Not today — the view only reflects current state. If a finding is wrong or you've decided to live with it, ignore it; it'll keep showing up until the underlying condition changes.

**Why is my disk 92% full but the "Act now" pill says 0?** Probably the probe hasn't run since the disk filled up. The Health view recomputes whenever metrics or inventory refresh — metrics polls every 3 seconds, inventory every 15 seconds. If the displayed number is stale, the corresponding finding will be too — give it a few seconds.

**The action button does nothing when I click it.** It should always at least fill the chat composer with the remediation prompt. If it visibly doesn't (composer stays empty), refresh the page and try again.

**Image updates are listed as one finding but I have 8 containers.** That's intentional — listing each individually would dominate the view. The action prompt lists all the affected image refs by name so Faro can review them together.

**What happens if my server is unreachable?** No probes run, no findings update. The Health view shows whatever state it last saw, and the top bar's server-pill switches to disconnected (red). See [Recover when SSH stops working](/help/recover-when-ssh-stops-working) for the path back.

## Reference

**Threshold values** (current defaults — these may shift as the heuristics get tuned):

| Check | Threshold |
|---|---|
| Disk full (red) | `metrics.diskPercent ≥ 90` |
| Disk warn (amber) | `metrics.diskPercent ≥ 80 && < 90` |
| RAM high (amber) | `(ramUsedMB / ramTotalMB) ≥ 90%` (cache included in `ramUsedMB`) |
| Container restarting | Docker status string matches `/restart/i` (e.g. "Restarting (1) 5 seconds ago") |
| Container stopped | Status is not empty AND does not start with `Up` AND not restarting |
| Site without TLS | A site has `domain` set but `tls` is false in the inventory |

**Refresh cadence** — metrics polls every **3 seconds**, inventory every **15 seconds**. Findings recompute on every refresh of either.

**Where the data comes from** — the same live metrics and inventory probe that drives the rest of the app (CPU/RAM/disk, running services, Docker containers), with per-image update detection layered on top. Findings are recomputed from that snapshot on every refresh; nothing is stored between refreshes.

---
