Skip to main content

Docker Deployment Guide

GAIA uses Docker Compose to orchestrate all the required services. This makes deployment simple and ensures consistency across different environments.
Looking for an easier setup? The GAIA CLI provides a guided wizard that handles Docker setup automatically, including prerequisite checks, environment variable discovery, and service management. This guide covers manual Docker setup for advanced users who prefer full control.

Prerequisites

Install Docker Desktop (recommended for beginners):Or install Docker Engine (for servers):
# Ubuntu/Debian (example)
sudo apt-get update
sudo apt-get install -y docker.io docker-compose-plugin
sudo systemctl enable --now docker

# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker
Minimum Requirements:
  • 2 CPU cores
  • 4GB RAM
  • 10GB free disk space
  • Docker Engine 20.10+
  • Docker Compose v2.0+
Recommended for Production:
  • 4+ CPU cores
  • 8+ GB RAM
  • 50+ GB SSD storage
  • Regular backups configured

Project Structure

When using the CLI quick start, running gaia init with Self-Host (Docker) scaffolds this structure automatically.
gaia/
├── apps/
│   ├── api/
│   │   ├── Dockerfile
│   │   ├── .env                # API environment variables
│   │   └── ...
│   ├── web/
│   │   ├── Dockerfile
│   │   ├── .env                # Web environment variables
│   │   └── ...
├── docs/
├── infra/
│   └── docker/
│       ├── docker-compose.yml      # Development compose file
│       └── docker-compose.prod.yml # Production compose file
└── ...

Quick Start

1

Install CLI and initialize self-host mode

npm install -g @heygaia/cli
gaia init
Choose Self-Host (Docker) in the setup wizard.
2

Start the stack

gaia start
3

Verify and monitor

gaia status
gaia logs
Access the applications:

Service Overview

The Docker Compose setup includes the following services:
Next.js React Application
  • Port: 3000
  • Hot reload enabled in development
  • Serves the web interface
web:
  container_name: gaia-web
  build: ./apps/web
  ports:
    - "3000:3000"
  environment:
    - NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
FastAPI Python Application
  • Port: 8000
  • Auto-reload enabled in development
  • RESTful API and WebSocket support
gaia-backend:
  container_name: gaia-backend
  build: ./apps/api
  ports:
    - "8000:80"
  command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--reload"]
PostgreSQL (Port: 5432)
  • Stores user data and application state
  • User: postgres, Password: postgres, DB: langgraph
MongoDB (Port: 27017)
  • Document storage and metadata
  • No authentication in development
Redis (Port: 6379)
  • Caching and session storage
  • No authentication in development
ChromaDB (Port: 8080)
  • Vector database for embeddings
  • Persistent storage in Docker volume
Worker - Handles scheduled tasks and background processing ARQ Worker - Async task queue processing RabbitMQ - Message broker (Port: 5672, Management: 15672)

Persistent Workspace Storage (JuiceFS and FUSE)

GAIA’s per-user workspace (file uploads, agent artifacts, installed skills) is backed by JuiceFS, a filesystem that stores file data in object storage (Cloudflare R2, S3, or any S3-compatible store) and file metadata in a database. The backend mounts it at /mnt/jfs and reads and writes files there like a local disk. JuiceFS mounts through FUSE (Filesystem in Userspace), a Linux kernel feature. This has direct consequences for where and how you can run the backend.

Run the backend in a container

Because the mount needs Linux FUSE, run the backend in a container on every platform:
Host OSNative (uvicorn on the host)Containerized (Docker)
LinuxWorks if juicefs is installed and /dev/fuse is accessibleWorks
macOSNot supported (no Linux FUSE)Works, Docker Desktop’s Linux VM provides /dev/fuse
WindowsNot supported (no Linux FUSE)Works, Docker Desktop / WSL2 provides /dev/fuse
The self-host Compose stack already runs the backend in a container, so all three platforms are covered. The native path is a development-only convenience on Linux.
Run the backend in a container even on macOS and Windows. Docker Desktop runs a Linux VM that exposes /dev/fuse, so the FUSE mount works inside the container even though the host OS cannot mount it directly.

How the mount works

FUSE lets a userspace program act as a filesystem instead of the kernel:
  1. The kernel exposes a device at /dev/fuse.
  2. The juicefs mount process opens that device and attaches itself as the driver for /mnt/jfs.
  3. When the backend reads or writes a path under /mnt/jfs, the kernel routes the call to the JuiceFS process.
  4. JuiceFS looks up metadata in its database, fetches or stores the file’s chunks in object storage, and serves the result back through /dev/fuse.
The backend never sees any of this, it just reads and writes files under /mnt/jfs.

Container settings that make it work

A Docker container is unprivileged by default: it cannot mount filesystems, cannot see host devices, and runs under an AppArmor profile that blocks mount. The Compose file grants exactly what FUSE needs and nothing more:
gaia-backend:
  environment:
    # Where the backend expects the mount. Must match the mountpoint.
    JUICEFS_HOST_MOUNT_PATH: /mnt/jfs
    # FUSE filesystems can't be watched with inotify, so artifact
    # detection tails the JuiceFS access log instead.
    ARTIFACT_DETECTION_MODE: accesslog
  cap_add:
    - SYS_ADMIN # the mount() syscall requires this capability
  devices:
    - /dev/fuse:/dev/fuse # pass the FUSE device into the container
  security_opt:
    - apparmor:unconfined # lift the default profile that denies mount
  volumes:
    - juicefs_cache:/var/cache/juicefs # keep the local chunk cache warm
The mount(2) syscall requires the CAP_SYS_ADMIN capability, which Docker drops by default. Without it, juicefs cannot perform the mount and fails before it starts. This grants only that one capability, not full privileged access.
The JuiceFS process opens /dev/fuse to talk to the kernel’s FUSE module. Containers see no host devices by default, so this passes the device through. Without it you get fusermount3: cannot open /dev/fuse: No such file or directory.
On Ubuntu and Debian hosts, Docker applies a default AppArmor profile that denies the mount operation even when the container holds CAP_SYS_ADMIN. This lifts that profile. On hosts without AppArmor it has no effect.
JuiceFS keeps a local disk cache of file chunks so repeat reads don’t go back to object storage. Persisting it in a named volume keeps the cache warm across restarts. This affects speed only, not correctness.
Avoid privileged: true as a shortcut. It grants every capability and device to the container, which is a much larger attack surface. The four scoped settings above are the least-privilege way to enable a single FUSE mount.

Required configuration

The container settings make a mount possible. JuiceFS still needs an object store and a metadata database before it will mount. Set these in apps/api/.env:
VariablePurpose
R2_ACCOUNT_ID, R2_BUCKET, R2_ACCESS_KEY, R2_SECRET_KEYCloudflare R2 (or S3-compatible) object store for file data
JUICEFS_META_URL_TEMPLATEConnection URL for the metadata database (a PostgreSQL DB works well)
Until both are configured, the backend starts normally and chat works, but workspace features (uploads, artifacts, skill installs) degrade and log JuiceFSUnavailable. See Environment Variables for the full list.

Docker Commands

Starting and Stopping

# Start all services
docker compose up -d

# Start specific services
docker compose up -d gaia-backend postgres redis

# Stop all services
docker compose down

# Stop and remove volumes (⚠️ deletes data)
docker compose down -v

Viewing Logs

# View logs for all services
docker compose logs -f

# View logs for specific service
docker compose logs -f gaia-backend

# View logs with timestamps
docker compose logs -f -t

# View last 100 lines
docker compose logs --tail=100 gaia-backend

Service Management

# Restart a service
docker compose restart gaia-backend

# Rebuild and restart a service
docker compose up -d --build gaia-backend

# Execute commands in running container
docker compose exec gaia-backend bash
docker compose exec postgres psql -U postgres -d langgraph

# Check service status
docker compose ps

Development Mode

For development, use the default docker-compose.yml:
# Start with hot reload
docker compose up -d

# View real-time logs
docker compose logs -f gaia-backend frontend
Development Features:
  • Hot reload for both frontend and backend
  • Source code mounted as volumes
  • Debug logging enabled
  • Development databases with default credentials

Health Checks

All services include health checks. Monitor service health:
# Check health status
docker compose ps

# Services with health checks:
# ✓ gaia-backend - HTTP health endpoint
# ✓ postgres - pg_isready
# ✓ redis - redis-cli ping
# ✓ mongo - mongosh ping
# ✓ chromadb - TCP connection check
# ✓ rabbitmq - rabbitmqctl status

Data Persistence

Data is persisted using Docker volumes:
# View volumes
docker volume ls | grep gaia

# Backup a volume
docker run --rm -v gaia-dev_pgdata:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz -C /data .

# Restore a volume
docker run --rm -v gaia-dev_pgdata:/data -v $(pwd):/backup alpine tar xzf /backup/postgres-backup.tar.gz -C /data
Volume Locations:
  • pgdata - PostgreSQL data
  • mongo_data - MongoDB data
  • redis_data - Redis data
  • chroma_data - ChromaDB vectors
  • rabbitmq_data - RabbitMQ messages

Networking

Services communicate through the gaia_network bridge network:
# Inspect network
docker network inspect gaia-dev_gaia_network

# Services can reach each other by container name:
# - gaia-backend → postgres:5432
# - gaia-backend → redis:6379
# - gaia-backend → chromadb:8000

Troubleshooting

The backend can’t reach the workspace mount. Check, in order:
  • Object store and metadata are configured: R2_* and JUICEFS_META_URL_TEMPLATE must be set in apps/api/.env. Without them JuiceFS never mounts.
  • FUSE settings are present, confirm cap_add: SYS_ADMIN, devices: /dev/fuse, and security_opt: apparmor:unconfined are on gaia-backend and arq_worker.
  • The mount came up, run docker compose exec gaia-backend mountpoint /mnt/jfs. A non-zero result means the mount failed; check docker compose logs gaia-backend for the JuiceFS error.
The FUSE device isn’t reaching the container. Confirm the devices: /dev/fuse:/dev/fuse mapping is present. On a Linux host, verify the device exists with ls -l /dev/fuse and load the module if missing: sudo modprobe fuse. On macOS and Windows, make sure Docker Desktop is running (it provides the device from its Linux VM).
The container holds the capability but the host’s security profile is blocking the mount. Confirm security_opt: apparmor:unconfined is set. On SELinux hosts, you may also need to allow the mount in your local policy.

Alternative: CLI Setup

For a simplified setup experience with automatic configuration, see the CLI Setup Guide. The CLI handles all the Docker commands, environment setup, and health checks automatically.

Next Steps

Environment Variables

Configure your API keys and settings