# Deploy on a server

> Set up DNS, run the one-command installer and log in on any Linux server with Docker.

Source: https://docs.grovs.io/docs/self-hosting/get-started/deployment

This is the generic install. For a guided setup, see [Platforms](https://docs.grovs.io/docs/self-hosting/platforms) or [Cloud VMs](https://docs.grovs.io/docs/self-hosting/clouds).

## Prerequisites

| Requirement | Value                                                                                                                                                 |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Server      | Linux with [Docker](https://docs.docker.com/get-docker/) and [Docker Compose v2](https://docs.docker.com/compose/install/), ports `80` and `443` open |
| Size        | 4 vCPU, 8 GB RAM, 80 GB SSD for up to 100,000 users. See [Sizing](https://docs.grovs.io/docs/self-hosting/operate/sizing).                            |
| Domain      | One domain you control, with DNS you can edit                                                                                                         |

## DNS

Grovs uses up to three domains. The installer asks for them in this order. Press Enter to accept each default.

| Prompt            | Variable      | What lives there                                        | Default                 |
| ----------------- | ------------- | ------------------------------------------------------- | ----------------------- |
| App domain        | `SERVER_HOST` | `dashboard.`, `api.`, `sdk.`, `go.`, `mcp.`, `preview.` | local trial on `lvh.me` |
| Links domain      | `DOMAIN_LIVE` | production links: `<project>.<links domain>`            | the app domain          |
| Test links domain | `DOMAIN_TEST` | test links: `<project>.<test links domain>`             | `test.<links domain>`   |

The usual setup is a branded short domain for links (`acme.link`) and the company domain for the app (`acme.com`). One domain for everything also works. The test links domain can be a sub-label of the links domain (the default) or a separate domain.

Create every record as an `A` record pointing at your server's IPv4. Add a matching `AAAA` if the server has IPv6.

| Domain     | Name (host)                                       | Serves                                                         |
| ---------- | ------------------------------------------------- | -------------------------------------------------------------- |
| app        | `dashboard`, `api`, `sdk`, `go`, `mcp`, `preview` | the fixed hosts                                                |
| links      | `*` **(wildcard)**                                | per-project production links, for example `a1b2c3d4.acme.link` |
| links      | `links`                                           | `LINKS_PROD_HOST`, with a pre-issued certificate               |
| test links | `*` **(wildcard)**                                | per-project test links, for example `a1b2c3d4.test.acme.link`  |
| test links | `links`                                           | `LINKS_TEST_HOST`, with a pre-issued certificate               |

With one domain for everything, that is the six fixed hosts plus `*` and `*.test`.

> **Warning:** The `*` wildcards are mandatory. Every project gets its own random link subdomain. Without the wildcards those subdomains return 404 and cannot get a TLS certificate.

### TLS and Universal Links

The standalone Caddy proxy issues certificates **on demand** for each new subdomain. The first request takes a few seconds. Apple's and Google's association fetchers can time out on that cold start and cache the failure for about an hour.

For reliable Universal Links and App Links, pre-issue wildcard certificates for the links and test links domains through your DNS provider's API. Behind Cloudflare, keep the link records **DNS-only (grey cloud)** so Caddy terminates TLS, or use a Cloudflare Origin certificate.

## Install

Once DNS points at the server, run:

```bash
curl -fsSL https://github.com/grovs-io/self-host/releases/latest/download/install.sh | bash
```

The installer comes from the latest completed release and selects its matching GHCR images. It:

1. Downloads the stack into `./grovs`.
2. Asks for your app domain, links domain, test links domain and admin email.
3. Generates every secret and pulls the images.
4. Starts the stack with the `standalone` proxy.

An exported `GROVS_VERSION` overrides its default version. Press Enter at the first prompt to start a local trial on `lvh.me` instead (see [Try locally](https://docs.grovs.io/docs/self-hosting/get-started/try-locally)).

### Manual install

[Read `install.sh`](https://github.com/grovs-io/self-host/releases/latest/download/install.sh) first if you prefer. The manual equivalent:

```bash
git clone https://github.com/grovs-io/self-host.git grovs && cd grovs
GROVS_RELEASE_URL="$(curl -fsSL -o /dev/null -w '%{url_effective}' https://github.com/grovs-io/self-host/releases/latest)"
GROVS_VERSION="${GROVS_RELEASE_URL##*/}" ./scripts/setup.sh  # writes .env and prints your admin password
docker compose --profile standalone pull
docker compose --profile standalone up -d       # drop the profile on platforms with their own proxy
```

`setup.sh` writes every hostname from your domains. It generates the database and ClickHouse passwords, the Rails secrets, the OAuth pair and the admin password. It keeps an existing `.env`. **Save the admin password it prints.**

On first start, `migrate` runs the PostgreSQL and ClickHouse migrations and the seed (the OAuth app and your bootstrap admin). Then `web` and the workers start. Certificates are issued on the first request to each host.

## First login

Open `https://<DASHBOARD_HOST>` and log in with the admin email and the password `setup.sh` printed. The bootstrap admin needs no SMTP or SSO.

Public sign-up is disabled. On first login the dashboard asks you to **create your first project**; there is no default project. Invite more members from the dashboard. Each invite produces a copyable link unless you configure SMTP.

A fresh install shows "Analytics temporarily unavailable" until the first rollup completes, about a minute after boot. Reload the page.

After you create a project, copy its **API key** for the [SDKs](https://docs.grovs.io/docs/self-hosting/configure/sdk-configuration). There is one key per environment, test and production.

## Verify

```bash
# Backend health
curl -sS https://<API_HOST>/up            # -> 200
```

Then in the dashboard: log in, create a project, create a link and open it. [Monitoring](https://docs.grovs.io/docs/self-hosting/operate/monitoring) covers ongoing health checks.

## Change settings

All settings live in `.env` in the install directory. Edit it, then apply:

```bash
docker compose --profile standalone up -d                                   # real domain
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d      # local trial
```

Compose restarts only the containers whose environment changed. The database, uploads and analytics stay.

- Never change the generated secrets after the first start: `SECRET_KEY_BASE`, the `ACTIVE_RECORD_ENCRYPTION_*` keys, and the database and ClickHouse passwords. Existing data is bound to them.
- If you change a domain, update DNS first.

Every variable is listed in [Configuration](https://docs.grovs.io/docs/self-hosting/configure/configuration). Before real traffic, work through the [production checklist](https://docs.grovs.io/docs/self-hosting/operate/production-checklist).
