Deployment
Deploy the full Grovs stack with Docker Compose — DNS, secrets, the one-command bring-up, and your first login.
The grovs-io/self-host repository brings up the entire platform with Docker Compose. This guide takes you from DNS to a working dashboard.
Prerequisites
- A Linux server with Docker and Docker Compose v2, ports
80/443open. - Two registrable domains (see below), with DNS you can edit.
- A comfortable floor is 4 vCPU / 8 GB RAM / 80 GB SSD (e.g. a Hetzner CX33-class box).
DNS and domains
Grovs serves production and test links from per-project subdomains, and the backend tells them apart by their registrable domain. So you need two separate registrable domains — one for production, one for test — exactly like the managed cloud uses sqd.link (prod) and test-sqd.link (test).
The test domain must NOT be a sub-label of the production domain. test-links.example.com (a subdomain of example.com) will not route — the host parser splits proj.test-links.example.com into subdomain proj.test-links + domain example.com, so the test project never matches. Use a distinct registrable domain such as example-test.com.
Create every record below as an A record pointing at your server's IPv4 (add a matching AAAA for IPv6 if your host has one). Replace example.com / example-test.com with your own two domains.
Production domain (DOMAIN_LIVE, e.g. example.com):
| Type | Host | Serves |
|---|---|---|
A | dashboard | Dashboard UI |
A | api | Dashboard API + asset blobs |
A | sdk | Mobile / server SDKs (the SDK baseURL) |
A | mcp | MCP OAuth/API |
A | go | Short-link helper |
A | links | Production links |
A | preview | Link previews |
A | * (wildcard) | Per-project production link subdomains |
Test domain (DOMAIN_TEST, a separate registrable domain, e.g. example-test.com):
| Type | Host | Serves |
|---|---|---|
A | links | Test links |
A | * (wildcard) | Per-project test link subdomains |
The * wildcard on each domain is mandatory — every project gets its own random link subdomain (e.g. a1b2c3d4.example.com). Without it those subdomains 404 and can't obtain a TLS certificate.
The test domain only carries the test links — the dashboard, API, and SDK are shared (the SDK uses the same sdk. host with useTestEnvironment to pick the environment).
Deploy the stack
Clone with submodules
git clone --recursive https://github.com/grovs-io/self-host.git grovs-self-hosted
cd grovs-self-hosted
# if you forgot --recursive:
git submodule update --init --recursiveGenerate secrets
The bundled script writes a .env with strong random secrets and a deterministic OAuth pair, and prints your bootstrap admin password:
./scripts/setup.shsetup.sh keeps the must-match pairs in sync for you — the DATABASE_URL password, the MinIO ⇄ S3 credentials, and NEXT_PUBLIC_CLIENT_ID / CLIENT_SECRET ⇄ the OAuth pair. Save the admin password it prints.
Set your domains
Edit .env and fill in your hosts and the two domains:
DOMAIN_LIVE=example.com # production registrable domain
DOMAIN_TEST=example-test.com # SEPARATE registrable domain for test
DASHBOARD_HOST=dashboard.example.com
API_HOST=api.example.com
SDK_HOST=sdk.example.com
MCP_HOST=mcp.example.com
GO_HOST=go.example.com
LINKS_PROD_HOST=links.example.com
LINKS_TEST_HOST=links.example-test.com
PREVIEW_HOST=preview.example.com
ACME_EMAIL=[email protected] # Let's Encrypt notices
BOOTSTRAP_ADMIN_EMAIL=[email protected]
# These must all point at your API + dashboard hosts:
SERVER_HOST=api.example.com
REACT_HOST=dashboard.example.com
S3_ASSET_PREFIX=https://api.example.com
NEXT_PUBLIC_API_URL=https://api.example.comSee Configuration for the full reference.
The NEXT_PUBLIC_* values are baked into the dashboard image at build time, so set them before the first build (changing them later requires a rebuild).
Build and start
docker compose --profile standalone build
docker compose --profile standalone up -dOn first start, a one-shot backend-migrate container runs the database migrations and seed (it creates the OAuth app and your bootstrap admin), then the web, worker, and dashboard services come up. The Caddy proxy obtains TLS certificates automatically.
First login
Open https://<DASHBOARD_HOST> and log in with BOOTSTRAP_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 (no email needed).
After creating a project, grab its API key (per environment — test vs 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="<ADMIN_PASSWORD>" \
-d client_id="<OAUTH_CLIENT_UID>" -d client_secret="<OAUTH_CLIENT_SECRET>"Upgrades
git submodule update --remote --merge # pull latest backend + dashboard
docker compose --profile standalone build
docker compose run --rm backend-migrate # migrate BEFORE restarting web/workers
docker compose --profile standalone up -dBackups
Durable state lives in named Docker volumes — back these up off-box:
pg_data— PostgreSQL, the system of record (pg_dump/ volume snapshots).minio_data— uploaded assets and exports (mc mirror/ snapshot).redis_data— AOF; only undrained events are at risk.
Next step
Configuration — environment variables, branding, and email →