Skip to content

Self-Host with Docker

Deploy Batida on your own infrastructure using Docker Compose. This guide covers everything you need to get a production-ready instance running.

Prerequisites

  • A Linux server (recommended: Ubuntu 22.04+) or any machine with Docker installed
  • Docker Engine 20.10+ and Docker Compose v2+
  • At least 2 GB RAM and 10 GB disk space
  • A domain name pointing to your server (optional, but recommended for TLS)

Architecture

Batida has three components you'll run:

ComponentDescriptionPort
batida-apiRust backend (REST API, event processing)5678
batida-webReact frontend (SPA)8080
PostgreSQLPrimary database5432
RedisCache and event idempotency6379
NATSEvent bus (JetStream enabled)4222

Step 1 — Create a Docker Compose File

Create a docker-compose.yml in your deployment directory:

yaml
version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: batida
      POSTGRES_USER: batida
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U batida"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  nats:
    image: nats:2-alpine
    restart: unless-stopped
    command: "--jetstream --store_dir /data"
    volumes:
      - nats_data:/data
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8222/healthz"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    image: git.batida.io/batida/batida-api:latest
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://batida:${POSTGRES_PASSWORD:-changeme}@postgres:5432/batida
      REDIS_URL: redis://redis:6379
      NATS_URL: nats://nats:4222
      APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:8080}
      APP_INTERNAL_PORT: "5678"
      ENVIRONMENT: ${ENVIRONMENT:-production}
      CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:8080}
      RUST_LOG: ${RUST_LOG:-info}
      # Generate each with: openssl rand -base64 32
      JWT_SECRET: ${JWT_SECRET:-changeme_jwt}
      ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET:-changeme_admin_jwt}
      ENCRYPTION_KEY: ${ENCRYPTION_KEY:-changeme_enc}
      # Email (optional)
      RESEND_API_KEY: ${RESEND_API_KEY:-}
      RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      nats:
        condition: service_healthy
    ports:
      - "5678:5678"

  web:
    image: git.batida.io/batida/batida-web:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    depends_on:
      - api

volumes:
  postgres_data:
  redis_data:
  nats_data:

networks:
  default:
    name: batida-network

WARNING

Change all default passwords before deploying. Generate each security key with openssl rand -base64 32. All three keys (JWT_SECRET, ADMIN_JWT_SECRET, ENCRYPTION_KEY) must be different.

Step 2 — Configure Environment

Create a .env file in the same directory:

bash
# Database
POSTGRES_PASSWORD=your-strong-password-here

# Security keys (generate each with: openssl rand -base64 32)
JWT_SECRET=your-jwt-secret-here
ADMIN_JWT_SECRET=your-admin-jwt-secret-here
ENCRYPTION_KEY=your-encryption-key-here

# Application
APP_PUBLIC_URL=https://batida.example.com
ENVIRONMENT=production
CORS_ORIGINS=https://batida.example.com

# Email (optional)
RESEND_API_KEY=re_xxxxxxxx
RESEND_FROM_EMAIL=noreply@example.com

Step 3 — Pull and Start

bash
docker-compose pull
docker-compose up -d

The API runs database migrations automatically on startup. Wait about 30 seconds for all services to become healthy:

bash
docker-compose ps

All services should show healthy or running.

Step 4 — Access Batida

Open your browser at:

http://your-server-ip:8080

Create your admin account and organization on first access.

TLS with a Reverse Proxy

For production, put Batida behind a reverse proxy with TLS. Here's a minimal Caddy example:

batida.example.com {
    reverse_proxy web:8080
    reverse_proxy /api/* api:5678
}

Or with Nginx:

nginx
server {
    listen 443 ssl http2;
    server_name batida.example.com;

    ssl_certificate /etc/ssl/certs/batida.pem;
    ssl_certificate_key /etc/ssl/private/batida.key;

    location / {
        proxy_pass http://web:8080;
        proxy_set_header Host $host;
    }

    location /api/ {
        proxy_pass http://api:5678;
        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;
    }
}

Updating

bash
docker-compose pull
docker-compose up -d

The API handles migrations on startup — no manual migration step needed.

Backups

PostgreSQL data is in a Docker volume. To back it up:

bash
docker-compose exec postgres pg_dump -U batida batida > backup_$(date +%Y%m%d).sql

To restore:

bash
cat backup_20260523.sql | docker-compose exec -T postgres psql -U batida batida

Troubleshooting

API won't start

Check logs with docker-compose logs api. The most common issue is the database not being ready — the API retries with backoff, so wait 60 seconds before investigating.

Database connection refused

Ensure PostgreSQL is healthy: docker-compose exec postgres pg_isready -U batida.

Events not processing

Check NATS health: docker-compose exec nats wget -qO- http://localhost:8222/healthz.

Port conflicts

If ports 5432, 6379, 4222, 5678, or 8080 are already in use, change the host-side port mappings in your docker-compose.yml (e.g., "5433:5432").

For more help, see Troubleshooting.

Built by the Batida team