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
| Port | Opened by | Who needs it |
|---|---|---|
80 | proxy | Everyone. Caddy uses it for certificate validation and redirects to HTTPS. |
443 | proxy | Everyone: dashboard, API, SDKs and links |
22 | Your server | Only 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:
| Direction | Rule |
|---|---|
| Inbound | TCP 80 and 443 from the internet |
| Inbound | TCP 22 from your own IP only |
| Outbound | DNS, 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:
ls -l .env # -rw------- expected
chmod 600 .envKeep 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.
| Variable | Rotate? |
|---|---|
ADMIN_API_KEY, DIAGNOSTICS_API_KEY, SENT_QUOTAS_WEBHOOK_KEY | Yes. Update any monitoring that sends DIAGNOSTICS_API_KEY. |
OAUTH_CLIENT_UID, OAUTH_CLIENT_SECRET | Yes. 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_KEY | Yes. Rotate them at the provider first. |
SECRET_KEY_BASE, ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY, ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY, ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT | Never. Existing data is bound to them, and changed encryption keys make encrypted columns unreadable. |
POSTGRES_PASSWORD, CLICKHOUSE_PASSWORD | Never 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_EMAILandBOOTSTRAP_ADMIN_PASSWORDonly when that account does not exist. Changing.envlater 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_PASSWORDin.env. - Turn on two-factor authentication for every account on the Account page.
- Remove members who no longer need access.
Updates
| Component | How to update |
|---|---|
| Grovs backend and dashboard | Upgrades |
| PostgreSQL, Redis, ClickHouse and Caddy images | docker compose --profile standalone pull, then up -d. The upgrade procedure does this too. |
| Ubuntu packages and Docker | sudo 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.