Docs
grovs.io Start for free

Security

Harden a Compose installation: open ports, firewall, secrets, TLS, admin accounts and updates.

The stack ships with private databases, generated secrets and automatic HTTPS. The steps below close what is left to you as the server owner.

Exposed ports

PortOpened byWho needs it
80proxyEveryone. Caddy uses it for certificate validation and redirects to HTTPS.
443proxyEveryone: dashboard, API, SDKs and links
22Your serverOnly you, for SSH

PostgreSQL, Redis, ClickHouse, web and dashboard publish no ports. They talk over the private Compose network, and only the proxy reaches web and dashboard. Keep it that way: do not add ports: entries for the databases.

The local trial override publishes web and dashboard directly on GROVS_WEB_PORT and GROVS_DASHBOARD_PORT. Never use it on a public server.

Firewall

Use your provider's firewall in front of the server:

DirectionRule
InboundTCP 80 and 443 from the internet
InboundTCP 22 from your own IP only
OutboundDNS, HTTPS and package downloads

Docker publishes ports through its own iptables rules, so a host firewall such as UFW does not filter them. A provider firewall does. The AWS, Google Cloud and Azure templates create these rules for you; see Cloud VMs.

Secrets and .env

.env holds every credential and the keys that encrypt data at rest. Setup creates it readable by its owner only. On cloud VMs the bootstrap also locks /opt/grovs to root. Check it:

Bash
ls -l .env      # -rw------- expected
chmod 600 .env

Keep an encrypted copy off the server with your backups. Never commit .env to a repository or paste it into a ticket.

Rotate secrets

Edit the value in .env, then apply it with docker compose --profile standalone up -d. Generate new random values the way setup does, for example openssl rand -hex 24.

VariableRotate?
ADMIN_API_KEY, DIAGNOSTICS_API_KEY, SENT_QUOTAS_WEBHOOK_KEYYes. Update any monitoring that sends DIAGNOSTICS_API_KEY.
OAUTH_CLIENT_UID, OAUTH_CLIENT_SECRETYes. migrate updates the dashboard's OAuth application and the dashboard reads the new pair at start.
SMTP_PASSWORD, AWS_S3_KEY_ID, AWS_S3_ACCESS_KEYYes. Rotate them at the provider first.
SECRET_KEY_BASE, ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY, ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY, ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALTNever. Existing data is bound to them, and changed encryption keys make encrypted columns unreadable.
POSTGRES_PASSWORD, CLICKHOUSE_PASSWORDNever after the first start. The databases keep the original password.

Losing .env means losing the encryption keys. A database backup without the matching .env cannot be fully restored.

TLS

The standalone Caddy proxy issues Let's Encrypt certificates for the fixed hosts at startup and for each project link host on its first request. Expiry notices go to ACME_EMAIL.

On-demand issuance accepts any hostname whose DNS points at your server. For a hardened setup, issue wildcard certificates for the links and test links domains through your DNS provider's API instead, which also makes Universal Links and App Links reliable. This needs a Caddy build with your DNS provider's module (see the Caddy DNS challenge docs) or your own proxy. Behind Cloudflare, keep link records DNS-only. See TLS and Universal Links.

Admin accounts

  • Public sign-up is off on self-hosted installations. New members join only through invites.
  • The seed creates the bootstrap admin from BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD only when that account does not exist. Changing .env later does not change the password.
  • Set up email, then reset the bootstrap admin's password from the dashboard's Account page. Then clear BOOTSTRAP_ADMIN_PASSWORD in .env.
  • Turn on two-factor authentication for every account on the Account page.
  • Remove members who no longer need access.

Updates

ComponentHow to update
Grovs backend and dashboardUpgrades
PostgreSQL, Redis, ClickHouse and Caddy imagesdocker compose --profile standalone pull, then up -d. The upgrade procedure does this too.
Ubuntu packages and Dockersudo apt-get update && sudo apt-get upgrade, or enable Ubuntu's unattended upgrades

After a reboot, Docker restarts the containers, and on cloud VMs grovs.service starts the stack. Check Monitoring after every update.

Last updated 2026-10-26
Was this page helpful?