Deployment
This guide covers deploying TideMeter to production environments.
Docker Deployment (Recommended)
Using the Docker Hub Image
Pre-built Docker images are available on Docker Hub . This is the fastest way to get started:
docker pull tidemeter/tidemeter:tidemeter-v0.1.3You can run it directly:
docker run -d \
-p 3700:3700 \
-e DATABASE_URL="postgresql://postgres:[email protected]:5432/tidemeter" \
-e PAYLOAD_SECRET="your-secret-key-minimum-32-characters" \
-e NEXT_PUBLIC_APP_URL="http://localhost:3700" \
tidemeter/tidemeter:tidemeter-v0.1.3Browse available tags on Docker Hub → tidemeter/tidemeter .
Using Docker Compose
The simplest way to deploy TideMeter with all dependencies is Docker Compose.
1. Clone and Configure
git clone https://github.com/tidemeter/tidemeter.git
cd tidemeter
cp .env.example .envEdit .env with your production values:
DATABASE_URL=postgresql://postgres:secure-password@postgres:5432/tidemeter
PAYLOAD_SECRET=your-very-long-random-secret-key-minimum-32-chars
SESSION_SALT_SECRET=another-random-secret-for-visitor-hashing
NEXT_PUBLIC_APP_URL=https://analytics.yourdomain.com
NODE_ENV=production
ANALYTICS_DB_TYPE=postgresql2. Build and Run
# PostgreSQL only
docker compose -f docker/docker-compose.yml up -d
# With ClickHouse
docker compose -f docker/docker-compose.yml -f docker/docker-compose.ch.yml up -d
# Demo mode (pre-seeded with [email protected] / demodemo + sample data)
docker compose -f docker/docker-compose.yml -f docker/docker-compose.demo.yml up -d3. Reverse Proxy
Place TideMeter behind a reverse proxy (Nginx, Caddy, Traefik) for HTTPS.
Nginx example:
server {
listen 443 ssl http2;
server_name analytics.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:3700;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Caddy example (automatic HTTPS):
analytics.yourdomain.com {
reverse_proxy localhost:3700
}VPS Deployment
System Requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 1 core | 2+ cores |
| RAM | 1 GB | 2+ GB |
| Disk | 10 GB | 20+ GB (depends on traffic) |
| OS | Any Linux with Docker | Ubuntu 22.04+ |
Manual Setup (Without Docker)
1. Install Dependencies
# Node.js 22+ via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 22
nvm use 22
# pnpm
npm install -g pnpm
# PostgreSQL
sudo apt install postgresql postgresql-contrib2. Build
cd tidemeter
pnpm install
DATABASE_URL="postgresql://user:pass@localhost:5432/tidemeter" pnpm build3. Run with PM2
npm install -g pm2
pm2 start npm --name "tidemeter" -- start
pm2 save
pm2 startupCloud Platforms
Railway
- Fork the TideMeter repository
- Connect Railway to your GitHub
- Add a PostgreSQL service
- Set environment variables
- Deploy
Fly.io
flyctl launch
flyctl postgres create
flyctl secrets set DATABASE_URL=... PAYLOAD_SECRET=...
flyctl deployDigitalOcean App Platform
- Create a new App from your GitHub repo
- Add a PostgreSQL database component
- Configure environment variables
- Deploy
Kubernetes (Flux GitOps)
TideMeter runs unchanged on Kubernetes. The container applies its own schema migrations and analytics SQL on every boot, so no init container or pre-deploy job is required — only a database with CREATE privileges and the standard env vars. Failed migrations cause the pod to fail readiness, so a broken upgrade is held back automatically while the previous version keeps serving traffic.
A minimal Deployment looks like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: tidemeter
spec:
replicas: 1
selector:
matchLabels: { app: tidemeter }
template:
metadata:
labels: { app: tidemeter }
spec:
containers:
- name: tidemeter
image: tidemeter/tidemeter:latest
ports:
- containerPort: 3000
envFrom:
- configMapRef: { name: tidemeter-config }
- secretRef: { name: tidemeter-secret }
startupProbe:
httpGet: { path: /api/health, port: 3000 }
periodSeconds: 10
failureThreshold: 30 # ~5 min for first init + DEMO_MODE seed
readinessProbe:
httpGet: { path: /api/health, port: 3000 }
periodSeconds: 10
livenessProbe:
httpGet: { path: /api/health, port: 3000 }
periodSeconds: 30The startupProbe is important: the first call to /api/health triggers Payload init, which runs the production migration step (payload.db.migrate()), analytics SQL migrations, and (when DEMO_MODE=true) the demo seed — that can take a minute or two. Once warm, /api/health returns instantly and is safe to use as the readiness/liveness target. If migrations fail, init re-throws and /api/health returns 503 — Kubernetes halts the rollout.
For multi-replica rollouts, the analytics migrator takes a Postgres advisory lock so only one replica applies a migration at a time; the others wait, then see it as already-applied and continue.
GitOps update flow
If you manage the cluster with Flux (or Argo), pin the image in your manifest and let CI bump it:
- Build & push the image to a registry (Docker Hub for the app, GHCR for the website).
- Clone the GitOps repo, patch
.spec.template.spec.containers[0].imagewithyq, commit, and push. - Flux notices the commit and reconciles the new image into the cluster.
Your CI system can be GitHub Actions, GitLab CI, Jenkins, or any other runner. The only requirement is write access to your GitOps repository and a non-interactive way to update YAML manifests.
For private GitOps repositories, store credentials as CI secrets (token, deploy key, or bot credentials) and avoid hardcoding repository URLs or credentials in workflow files.
Production Checklist
Before going live, ensure:
-
NODE_ENV=productionis set -
PAYLOAD_SECRETuses a strong random value (32+ characters) -
SESSION_SALT_SECRETis set to a strong random value -
NEXT_PUBLIC_APP_URLpoints to your actual domain - HTTPS is configured (via reverse proxy or platform)
- Database backups are scheduled
- Resource monitoring is in place
- Rate limiting is configured at the reverse proxy level
Updating
git pull origin main
pnpm install
pnpm build
# Restart the application
pm2 restart tidemeter
# Or with Docker:
docker compose -f docker/docker-compose.yml up -d --build