Deployment
Deploy the full Grovs stack with Docker Compose — DNS, the one-command install, first login, upgrades, and backups.
The grovs-io/self-host repository brings up the entire platform with Docker Compose from published images. This guide takes you from DNS to a working dashboard.
For platform-specific setup, choose Coolify, Dokploy, AWS, Google Cloud, Railway or Render. For a laptop trial, follow Try locally.
Prerequisites
- A Linux server with Docker and Docker Compose v2, ports
80/443open. - One domain you control, with DNS you can edit.
- Start with 4 vCPU / 8 GB RAM / 80 GB SSD and size it for your event volume, retention and query workload.
Try it on your machine first
No domain, no DNS, no TLS. Run the installer and press Enter at the first prompt:
curl -fsSL https://github.com/grovs-io/self-host/releases/latest/download/install.sh | bashIt starts the same stack on lvh.me, a public name that resolves to 127.0.0.1, with the dashboard at http://dashboard.lvh.me:3002 and the API at http://api.lvh.me. Log in with the printed admin password, create a project and a link, and open it. Real deep links into apps need a real domain; everything else works. Change the ports with GROVS_WEB_PORT and GROVS_DASHBOARD_PORT before running it. Remove it with docker compose -f docker-compose.yml -f docker-compose.local.yml down -v in the install directory.
Works on Linux and macOS with Docker, and on Windows through WSL2 with Docker Desktop.
DNS
Grovs uses up to three domains. The installer asks for them in this order, each with a default you accept by pressing Enter:
| 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-environment links: <project>.<test links domain> | test.<links domain> |
A branded short domain for links (acme.link) next to the company domain for the app (acme.com) is the usual setup; one domain for everything also works. The test links domain can be a sub-label of the links domain — the default is — or a separate domain.
Create every record as an A record pointing at your server's IPv4 (add a matching AAAA if it has IPv6):
| Domain | Name (host) | Serves |
|---|---|---|
| app | dashboard, api, sdk, go, mcp, preview | the fixed hosts |
| links | * (wildcard) | per-project production links, e.g. a1b2c3d4.acme.link |
| links | links | LINKS_PROD_HOST, with a pre-issued certificate |
| test links | * (wildcard) | per-project test links, e.g. 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 on it.
The * wildcards are mandatory — every project gets its own random link subdomain. Without them those subdomains 404 and cannot obtain a TLS certificate.
TLS and Universal Links. The standalone Caddy proxy issues certificates on demand for each new subdomain, and the first hit takes a few seconds. For reliable Universal Links and App Links, pre-issue wildcard certificates for the links and test-links domains through your DNS provider's API; otherwise Apple's and Google's association fetchers can time out on the cold start and cache the failure for about an hour. Behind Cloudflare, keep the link records DNS-only (grey cloud) so Caddy terminates TLS, or use a Cloudflare Origin certificate.
Deploy the stack
Once DNS points at the host, one command:
curl -fsSL https://github.com/grovs-io/self-host/releases/latest/download/install.sh | bashThe installer comes from the latest completed release and selects its matching GHCR images. It downloads the stack into ./grovs, asks for your app domain, links domain, test links domain, and admin email, generates every secret, pulls the images, and starts the stack with the standalone proxy. An exported GROVS_VERSION overrides its default. Read install.sh first if you prefer; the manual equivalent is:
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 proxysetup.sh writes every hostname from the domains you enter, generates the database and ClickHouse passwords, the Rails secrets, the OAuth pair, and the admin password, and keeps an existing .env if there is one. Save the admin password it prints. On initial deployment the migrate service runs the PostgreSQL and ClickHouse migrations and the seed (the OAuth app and your bootstrap admin) before web and the workers come up. 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. No SMTP or SSO is required for the bootstrap admin.
Self-hosted instances disable public sign-ups. You log in as the bootstrap admin, and on first login the dashboard prompts you to create your first project (there is no pre-created default project). Invite additional 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 run completes, about a minute after boot. Reload.
After creating a project, grab its API key (one per environment — test and production) for the SDKs.
Verify
# Backend health
curl -sS https://<API_HOST>/up # -> 200
# Bootstrap-admin login returns an access token
curl -s -X POST https://<API_HOST>/oauth/token \
-d grant_type=password \
-d email="<BOOTSTRAP_ADMIN_EMAIL>" -d password="<BOOTSTRAP_ADMIN_PASSWORD>" \
-d client_id="<OAUTH_CLIENT_UID>" -d client_secret="<OAUTH_CLIENT_SECRET>"Then in the dashboard: log in, create a project, create a link, open it.
Changing settings
Everything is configured by .env in the install directory. Edit it, then apply:
docker compose --profile standalone up -d # real domain
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d # local trialCompose restarts only the containers whose environment changed; the database, uploads, and analytics stay. Two rules: never change the generated secrets (SECRET_KEY_BASE, the ACTIVE_RECORD_ENCRYPTION_* keys, the database and ClickHouse passwords) after the first start, because the existing data is bound to them; and if you change a domain, update DNS first.
Upgrades
Back up databases, uploads and .env, then check the latest release and its notes. Print its version without changing your running installation:
GROVS_RELEASE_URL="$(curl -fsSL -o /dev/null -w '%{url_effective}' https://github.com/grovs-io/self-host/releases/latest)"
printf 'GROVS_VERSION=%s\n' "${GROVS_RELEASE_URL##*/}"Set the GROVS_VERSION line in .env to that value. To follow the moving image tags on every upgrade, use GROVS_VERSION=latest instead. Clear any shell override so Compose reads the saved value, check the image references, then use a short maintenance window:
unset GROVS_VERSION
docker compose --profile standalone config --images
docker compose --profile standalone pull
docker compose --profile standalone stop dashboard web worker-1 worker-2
docker compose --profile standalone run --rm migrate
docker compose --profile standalone up -d
docker compose --profile standalone ps -aIf migrations fail, resolve the logged error before starting the application. Setup preserves an existing .env; edit its version explicitly. Use this migration sequence for upgrades—the installer also rewrites the saved version and is intended for initial setup. Never regenerate encryption keys during an upgrade. Follow the platform guide for Railway, Render, Coolify or Dokploy updates. See Images and releases for how numbered and latest tags behave.
Backups
Keep an encrypted off-server copy of .env or the platform environment. The encryption keys and credentials are needed when restoring your data.
Durable state lives in named Docker volumes — back these up off-box:
pg_data— PostgreSQL, the system of record: projects, links, users, purchases (pg_dumpor volume snapshots).clickhouse_data— events and analytics (volume snapshot orclickhouse-backup).storage— uploaded images and exports.redis_data— AOF; only undrained events are at risk.
Next step
Configuration — environment variables, optional features, branding, and email →