TideMeter
Skip to Content
Deployment

Deployment

This guide covers deploying TideMeter to production environments.

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.3

You 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.3

Browse 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 .env

Edit .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=postgresql

2. 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 -d

3. 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

ResourceMinimumRecommended
CPU1 core2+ cores
RAM1 GB2+ GB
Disk10 GB20+ GB (depends on traffic)
OSAny Linux with DockerUbuntu 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-contrib

2. Build

cd tidemeter pnpm install DATABASE_URL="postgresql://user:pass@localhost:5432/tidemeter" pnpm build

3. Run with PM2

npm install -g pm2 pm2 start npm --name "tidemeter" -- start pm2 save pm2 startup

Cloud Platforms

Railway

  1. Fork the TideMeter repository
  2. Connect Railway to your GitHub
  3. Add a PostgreSQL service
  4. Set environment variables
  5. Deploy

Fly.io

flyctl launch flyctl postgres create flyctl secrets set DATABASE_URL=... PAYLOAD_SECRET=... flyctl deploy

DigitalOcean App Platform

  1. Create a new App from your GitHub repo
  2. Add a PostgreSQL database component
  3. Configure environment variables
  4. 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: 30

The 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:

  1. Build & push the image to a registry (Docker Hub for the app, GHCR for the website).
  2. Clone the GitOps repo, patch .spec.template.spec.containers[0].image with yq, commit, and push.
  3. 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=production is set
  • PAYLOAD_SECRET uses a strong random value (32+ characters)
  • SESSION_SALT_SECRET is set to a strong random value
  • NEXT_PUBLIC_APP_URL points 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