>
---
name: appgarden
description: >
Deploy and manage web applications on remote servers with AppGarden.
Use when the user asks about deploying apps, configuring appgarden.toml,
managing AppGarden servers/apps, viewing logs/status/metadata, or exposing
local work through AppGarden tunnels.
user-invocable: false
---
# AppGarden CLI Skill
Use this skill when deploying or operating web applications with the `appgarden` CLI.
AppGarden is an agentless deploy tool: the local CLI connects over SSH, prepares the remote server, uploads or clones app source, writes Caddy/systemd/Docker files, and records state under the server's AppGarden root (default `/srv/appgarden`).
## Before running commands
- Prefer running from the AppGarden repo checkout when developing/testing AppGarden itself:
```bash
cd /path/to/appgarden && uv run appgarden ...
```
If AppGarden is installed in the environment, `appgarden ...` is also fine.
- Read-only/safe discovery commands are fine to run without confirmation: `appgarden --help`, `appgarden version`, `appgarden config show`, `appgarden server list`, `appgarden server ping`, `appgarden apps list`, `appgarden apps status`, `appgarden apps logs`, `appgarden tunnel list`.
- Ask before commands that create, mutate, deploy, restart, stop, delete, or expose services: `server add`, `server remove`, `server default`, `server init`, `deploy`, `apps start|stop|restart|redeploy|remove`, `apps meta set|replace|remove`, `tunnel open|close|cleanup`.
- Be especially careful with:
- `appgarden server init --include ssh`: hardens SSH configuration and reloads sshd.
- `appgarden server init --include firewall`: enables UFW and changes firewall policy.
- `appgarden apps remove NAME --yes`: deletes app resources; add `--keep-data` only when preserving the app's `data/` directory is intended.
- `appgarden deploy`: may overwrite uploaded source for an existing app and may update Caddy/systemd/Docker resources.
- `appgarden tunnel open`: publicly exposes a local port/file/directory through the configured server until stopped.
## Configuration locations
Local config is stored at:
```text
~/.config/appgarden/config.toml
```
Server config shape:
```toml
default_server = "myserver"
[defaults]
method = "dockerfile"
container_port = 3000
[servers.myserver]
host = "203.0.113.10" # or hcloud_name + hcloud_context
ssh_user = "root"
ssh_key = "~/.ssh/id_rsa"
domain = "apps.example.com"
app_root = "/srv/appgarden" # optional; defaults to /srv/appgarden
[servers.myserver.init]
skip = ["upgrades"] # optional persistent init skips
```
Remote state and resources are under `app_root`, usually:
```text
/srv/appgarden/garden.json # app registry
/srv/appgarden/ports.json # allocated ports
/srv/appgarden/apps/<name>/ # per-app files/source/app.json/.env/data
/srv/appgarden/caddy/apps/*.caddy # deployed app Caddy snippets
/srv/appgarden/caddy/tunnels/*.caddy
/srv/appgarden/tunnels/active.json
```
`appgarden config show` prints the local config. Use `--server <name>` / `-s <name>` or `APPGARDEN_SERVER` on commands that target a server; otherwise AppGarden uses `default_server`.
## Server setup
A server must be configured, reachable over SSH, and have DNS pointing at it.
```bash
appgarden server add myserver \
--host <server-ip-or-hostname> \
--ssh-user root \
--ssh-key ~/.ssh/id_rsa \
--domain apps.example.com
# Hetzner alternative: resolve IP through hcloud instead of --host
appgarden server add myserver \
--hcloud-name <hetzner-server-name> \
--hcloud-context <hcloud-context> \
--domain apps.example.com
appgarden server list
appgarden server ping myserver
appgarden server default myserver
```
For subdomain deployments and tunnels, create wildcard DNS such as `*.apps.example.com` pointing at the server IP. Explicit custom hostnames also need DNS pointing at the server.
### Initializing a server
Run once per server:
```bash
appgarden server init myserver
```
Current `server init` behavior:
- Optional steps **on by default**; skip with `--skip`: `update`, `docker`, `caddy`, `upgrades`.
- Opt-in steps **off by default**; enable with `--include`: `firewall`, `ssh`, `fail2ban`, `group`.
- Essential steps always run: configure the AppGarden Caddyfile import block, create directories, install the privileged helper/sudoers entry, set ownership for the deploy user, initialize state files, and start Docker/Caddy where available.
Examples:
```bash
appgarden server init myserver --minimal # essential steps only
appgarden server init myserver --skip upgrades # skip unattended-upgrades
appgarden server init myserver --include group # create/use appgarden group for non-root deploy users
appgarden server init myserver --include firewall # configure UFW; confirm first
appgarden server init myserver --include ssh # harden sshd; confirm first
```
For non-root SSH users, `server init` installs `/usr/local/bin/appgarden-privileged` and a sudoers rule restricted to AppGarden-scoped privileged operations.
## Preferred deployment flow: `appgarden.toml`
Create `appgarden.toml` in the project root and deploy named environments.
```toml
[app]
name = "myapp"
slug = "my-app" # optional; used by {app.slug}; falls back to name
server = "myserver"
method = "dockerfile" # static | command | dockerfile | docker-compose | auto
source = "." # local path or git URL
container_port = 3000
subdomain = "{app.slug}" # or path/url; supports placeholders
exclude = ["node_modules", ".git"]
gitignore = true # local uploads honor .gitignore by default
meta = { team = "frontend" }
[environments.production]
branch = "main"
subdomain = "{app.slug}"
env = { NODE_ENV = "production" }
[environments.staging]
branch = "staging"
subdomain = "{app.slug}-staging"
env_file = ".env.staging"
meta = { visibility = "internal" }
```
Deploy:
```bash
appgarden deploy production
appgarden deploy staging
appgarden deploy --all-envs
appgarden deploy --project /path/to/project production
```
If `appgarden.toml` is present and no environment/name is provided, `appgarden deploy` deploys all environments. If a positional environment is provided, it must exist.
### `appgarden.toml` semantics
- `[app].name` is required. Deployed app names are derived from it: `production` uses the base name, every other environment uses `<name>-<env>`. Example: `myapp` and `myapp-staging`.
- Values cascade in this order: hardcoded defaults < global `[defaults]` in `~/.config/appgarden/config.toml` < `[app]` defaults < `[environments.<name>]` < CLI flags / `APPGARDEN_*` environment variables.
- Dict fields `env` and `meta` merge by key; environment/CLI values override earlier keys.
- List fields `exclude` and `volumes` concatenate and deduplicate across layers.
- String values in project/environment config support placeholders: `{app.name}`, `{app.slug}`, `{env.name}`.
- Relative local `source` and `env_file` values resolve relative to the project directory or the directory containing the explicit `--project` file.
Useful fields in `[app]` or `[environments.<name>]`:
| Field | Meaning |
| --- | --- |
| `server` | Configured server name. |
| `method` | `static`, `command`, `dockerfile`, `docker-compose`, or `auto`. Default is `static`. |
| `source` | Local directory/path or git URL. Required for `static`, `dockerfile`, `docker-compose`, `auto`; optional for `command`. |
| `url` | Explicit public hostname/path, e.g. `example.com` or `example.com/docs`. |
| `subdomain` | Prefix combined with `domain` or the server domain, e.g. `docs` -> `docs.apps.example.com`. |
| `path` | Path under `domain` or server domain, e.g. `docs` -> `apps.example.com/docs`. |
| `domain` | Override the server's base domain for `subdomain`/`path`. |
| `port` | Host port; omitted means AppGarden auto-allocates from its port state. |
| `container_port` | Port exposed inside Dockerfile/auto containers. Default is `3000`. |
| `cmd` | Start command for `command` and `auto` methods. Required for both. |
| `setup_cmd` | Install/build command for `auto`; overrides the detected runtime default. |
| `branch` | Git branch for git sources. |
| `env` | Inline environment variables. |
| `env_file` | Local dotenv file. |
| `meta` | Arbitrary metadata stored with the app and shown in status. |
| `exclude` | Rsync exclude patterns for local uploads; also reused by `apps redeploy`. |
| `gitignore` | Whether local uploads pass rsync's `.gitignore` filter. Default `true`; CLI override is `--no-gitignore`. |
| `volumes` | Docker volume mounts (`host:container[:opts]`) for `dockerfile` and `auto`. |
| `created_at`, `updated_at`, `repo` | Optional metadata overrides. Dates accept short date, ISO string, or TOML dates; repo is otherwise auto-detected from local git origin when possible. |
URL resolution order is: explicit `url`; otherwise `subdomain` + domain; otherwise `path` + domain. For subdirectory routing (`domain/path`), multiple apps on the same domain share a generated Caddy config and AppGarden checks URL conflicts.
## Ad-hoc deployment without `appgarden.toml`
Use `--name` and flags. Most flags also have `APPGARDEN_*` environment variable equivalents (`APPGARDEN_NAME`, `APPGARDEN_SERVER`, `APPGARDEN_METHOD`, `APPGARDEN_SOURCE`, `APPGARDEN_URL`, `APPGARDEN_SUBDOMAIN`, `APPGARDEN_PATH`, `APPGARDEN_DOMAIN`, `APPGARDEN_PORT`, `APPGARDEN_CONTAINER_PORT`, `APPGARDEN_CMD`, `APPGARDEN_SETUP_CMD`, `APPGARDEN_BRANCH`, `APPGARDEN_ENVVAR_FILE`, `APPGARDEN_PROJECT`, `APPGARDEN_ALL_ENVS`).
```bash
# Static site / SPA
appgarden deploy --name mysite --method static --source ./dist --subdomain mysite
# Dockerfile app
appgarden deploy --name myapp --method dockerfile --source . --container-port 3000 --subdomain myapp
# Auto-generated Dockerfile; runtime is detected from package.json, requirements.txt,
# pyproject.toml, Gemfile, go.mod, or Cargo.toml. --cmd is required.
appgarden deploy --name myapp --method auto --source . --cmd "npm start" --subdomain myapp
# Bare process managed by systemd. --source is optional; --cmd is required.
appgarden deploy --name myapi --method command --source ./api --cmd "python app.py" --subdomain myapi
# Docker Compose stack; your compose file is used directly.
appgarden deploy --name mystack --method docker-compose --source ./project --subdomain mystack
# Explicit path routing instead of subdomain routing
appgarden deploy --name docs --method static --source ./docs --url apps.example.com/docs
# Env, metadata, excludes, and volumes
appgarden deploy --name myapp --method dockerfile --source . --subdomain myapp \
--envvar NODE_ENV=production \
--envvar-file .env.production \
--meta team=backend --meta visibility=internal \
--exclude node_modules --exclude .git \
--volume ./data:/app/data \
--no-gitignore
```
Deployment method notes:
| Method | Use for | Required inputs | Runtime behavior |
| --- | --- | --- | --- |
| `static` | HTML/CSS/JS/SPAs | `source` | Upload/clone source and serve directly with Caddy. |
| `command` | A local process without Docker | `cmd` (`source` optional) | Creates a systemd service with `PORT` set to allocated/explicit port. |
| `dockerfile` | Projects with a Dockerfile | `source` | Builds image remotely, writes AppGarden compose file, runs through systemd. |
| `docker-compose` | Existing compose projects | `source` | Runs your `docker compose up/down` through systemd; `volumes` config is not injected. |
| `auto` | Simple Node/Python/Ruby/Go/Rust projects | `source`, `cmd` | Detects runtime, writes Dockerfile, builds image, writes compose file. |
For local uploads, AppGarden uses rsync with `--delete`, honors `.gitignore` by default, and applies any `--exclude`/`exclude` patterns. For git sources, it clones the repo on the remote and optionally checks out the requested branch.
Environment variable precedence for deployed app `.env` files is: `appgarden.toml` `env` < `env_file` content < CLI `--envvar` values.
## Managing deployed apps
```bash
appgarden apps list [-s server] [--short]
appgarden apps status <name> [-s server]
appgarden apps logs <name> [-s server] [-n 100]
appgarden apps restart <name> [-s server]
appgarden apps redeploy <name> [-s server]
appgarden apps stop <name> [-s server]
appgarden apps start <name> [-s server]
appgarden apps remove <name> [-s server] [--keep-data] [--yes]
```
Operational guidance:
- Use `apps list` first when you do not know the exact app name.
- Use `apps status` to inspect URL, routing, method, port, repo/source, timestamps, and metadata.
- Use `apps logs NAME -n 200` before restarting when debugging.
- `apps redeploy` updates source: git apps run `git pull` (with the stored branch if present); local-source apps re-upload with stored `exclude`/`gitignore`; Dockerfile/auto apps rebuild images and preserve stored volumes; static apps reload Caddy.
- If changing an app name in `appgarden.toml`, remove the old app first to avoid URL/Caddy conflicts:
```bash
appgarden apps remove old-name --yes
appgarden deploy production
```
## Metadata
Metadata is arbitrary JSON-compatible key/value data stored with the app.
```bash
appgarden apps meta get myapp [-s server]
appgarden apps meta set myapp --meta team=backend --meta tier=premium [-s server]
appgarden apps meta replace myapp --json '{"team":"frontend"}' [-s server]
appgarden apps meta remove myapp tier visibility [-s server]
```
In `appgarden.toml`, `meta` dictionaries merge like `env` dictionaries:
```toml
[app]
name = "myapp"
meta = { team = "backend", visibility = "internal" }
[environments.production]
meta = { visibility = "public" }
# result: { team = "backend", visibility = "public" }
```
## Localhost tunnels
Use tunnels to expose a local dev server, file, directory, or command through the remote server with HTTPS. A tunnel blocks until Ctrl+C unless `--close-on-cmd-exit` is used with `--cmd`/`--serve`.
```bash
# Expose an existing local server. URL omitted => random three-word subdomain.
appgarden tunnel open 3000
# Use server domain subdomain shorthand or an explicit URL
appgarden tunnel open 3000 --subdomain preview
appgarden tunnel open 3000 --url preview.apps.example.com
# Run a local command while the tunnel is open
appgarden tunnel open 3000 --cmd "npm run dev"
appgarden tunnel open 3000 --cmd "npm run dev" --close-on-cmd-exit
# Serve a local file or directory. LOCAL_PORT is optional with --serve.
appgarden tunnel open --serve ./dist --subdomain docs-preview
appgarden tunnel open --serve ./dist --include "*.html" --exclude "node_modules"
appgarden tunnel list [-s server]
appgarden tunnel close <tunnel-id> [-s server]
appgarden tunnel cleanup [-s server]
```
`--cmd` and `--serve` are mutually exclusive. `--include`/`--exclude` only apply when `--serve` points to a directory. Tunnels allocate a remote port, write a Caddy snippet under `caddy/tunnels`, and register state in `tunnels/active.json`; cleanup removes those resources.
## Troubleshooting workflow
1. Confirm config and server targeting:
```bash
appgarden config show
appgarden server list
appgarden server ping <server>
```
2. Inspect app state and logs:
```bash
appgarden apps list --short
appgarden apps status <app>
appgarden apps logs <app> -n 200
```
3. Redeploy or restart only after reading the error:
```bash
appgarden apps redeploy <app>
appgarden apps restart <app>
```
4. Common local-source upload issues:
- `rsync` must be installed locally.
- Encrypted SSH keys need an agent: `eval $(ssh-agent) && ssh-add ~/.ssh/id_rsa`.
- Permission errors on non-root deploys usually mean rerun `appgarden server init --include group` or fix remote ownership.
## Reference files in this repository
The generated `src/appgarden/*.py` files say "AUTOGENERATED"; implementation edits should usually be made in the corresponding `pts/appgarden/*.pct.py` source files. For skill/use questions, read these files as needed:
- `README.md` — user-facing guide and CLI reference.
- `src/appgarden/cli.py` / `pts/appgarden/10_cli.pct.py` — command definitions and options.
- `src/appgarden/config.py` / `pts/appgarden/00_config.pct.py` — local config model and default config path.
- `src/appgarden/environments.py` / `pts/appgarden/08_environments.pct.py` — `appgarden.toml` parsing, merge semantics, placeholders.
- `src/appgarden/server.py` / `pts/appgarden/04_server.pct.py` — server init steps.
- `src/appgarden/deploy.py` / `pts/appgarden/05_deploy.pct.py` — deployment behavior and remote state.
- `src/appgarden/apps.py` / `pts/appgarden/06_apps.pct.py` — app lifecycle and redeploy behavior.
- `src/appgarden/routing.py` / `pts/appgarden/03_routing.pct.py` — Caddy routing and URL conflict behavior.
- `src/appgarden/tunnel.py` / `pts/appgarden/09_tunnel.pct.py` — tunnel behavior.
Creator's repository · lukastk/appgarden