Production Deployment
This guide covers deploying Otto on your own infrastructure for production use.
This guide covers deploying Otto on your own infrastructure for production use.
Prerequisites
Hardware Requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8 GB |
| Storage | 20 GB | 50 GB+ |
| OS | Linux, macOS, or Windows with Docker | Linux (Ubuntu 22.04+ or Debian 12+) |
Otto's resource usage scales primarily with the number of concurrent agent tasks. The ARQ worker is configured for up to 4 simultaneous jobs by default.
Software Requirements
- Docker and Docker Compose (v2+)
- curl (used by the CLI for health checks and updates)
No other runtime dependencies are required. All services run in containers.
Installation
Step 1: Clone the Repository
Step 2: Configure
Run the interactive setup wizard:
The wizard walks through each configuration section:
- LLM Provider (required) -- Choose Gemini, OpenAI, or Ollama and provide your API key
- Web Search -- Tavily API key for web search capability
- Specialization -- Focus area for the agent (e.g., "marketing assistant")
- Slack Integration -- Bot token and signing secret
- Email Integration -- SMTP and IMAP settings
- Redis Password -- Recommended for production
- LangSmith Tracing -- Optional observability
You can re-run the wizard at any time to change settings. Pressing Enter at any prompt keeps the current value. See Configuration for a detailed explanation of every setting.
Step 3: Start Otto
This pulls the latest container images from the GitHub Container Registry, creates the necessary data directories, and starts all services. On first run, expect the image pull to take a few minutes depending on your connection speed.
Otto will be running at http://localhost:3000.
Architecture Overview
A production Otto deployment runs four services managed by Docker Compose:
Frontend -- A statically exported Next.js application served by nginx. Handles the web UI and reverse-proxies API requests to the backend.
Backend API -- FastAPI server handling REST API requests, user management, file uploads, Slack webhooks, and MCP server management.
ARQ Worker -- Async task processor that runs agent tasks using LangGraph. Handles task execution, subtask delegation, project summarization, and scheduled tasks. Configured for up to 4 concurrent jobs with a 30-minute timeout.
Redis -- Provides task queues (interactive and background priority), LangGraph checkpoint storage, conversation state caching, and distributed locks for subagent coordination.
All services share a Docker network. The frontend nginx proxies API paths to the backend using Docker's internal DNS. Redis is not exposed to the host in production mode.
Reverse Proxy Setup
In production, Otto should sit behind a reverse proxy that handles TLS termination. The frontend listens on port 3000 and already proxies API requests to the backend internally, so you only need to expose a single port.
Option A: nginx
After configuring nginx, update your .env to reflect the public URL:
Option B: Cloudflare Tunnel
Cloudflare Tunnels provide HTTPS access without managing certificates or opening inbound ports. Otto includes built-in support for this.
-
Create a tunnel in your Cloudflare dashboard and obtain a tunnel token.
-
Add the token to your
.env: -
Start Otto with the tunnel profile:
This starts an additional
cloudflaredcontainer that establishes an outbound connection to Cloudflare's network and routes traffic to your Otto frontend. -
In Cloudflare, configure the tunnel's public hostname to route to
http://frontend:3000.
SSL/TLS Considerations
- Always run Otto behind HTTPS in production. The backend sets session cookies and handles API keys that must be encrypted in transit.
- If using a self-signed certificate for internal deployments, configure your clients to trust the CA.
- Let's Encrypt with certbot is a solid free option for publicly accessible deployments.
- The built-in nginx in the frontend container already sets security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection) and enables gzip compression.
Data Persistence
All persistent data lives under the data/ directory in the project root, bind-mounted into containers. Nothing is stored inside the containers themselves, so you can freely recreate them without data loss.
Storage Layout
What Each Store Contains
SQLite (data/sqlite/otto.db) -- All structured application data: user accounts, tasks and their conversation logs, output artifacts, notifications, Slack channel projects and summaries, scheduled tasks, and channel memberships. Runs in WAL mode for concurrent access and crash recovery.
Redis (data/redis/) -- Task queues (interactive and background priority), LangGraph agent execution checkpoints, conversation state cache (24-hour TTL), MCP configuration, pending message buffers for project summarization, and distributed locks. Persisted via AOF (fsync every second) with RDB snapshots as a secondary mechanism.
Team Memory (data/team_memory/otto_memory.db) -- A separate SQLite database storing vector embeddings (768-dimensional) that enable the agent to recall information across sessions. Uses cosine similarity search.
Sandbox (data/sandbox/) -- Working directory where the agent reads and writes files during task execution.
Uploads (data/uploads/) -- Files uploaded by users through the web UI.
Backup and Restore
Creating a Backup
Otto must be running when you create a backup (the script needs access to the Redis container).
This creates a timestamped directory under backups/:
The backup script triggers a Redis BGSAVE before copying the dump file to ensure consistency.
Restoring from a Backup
The restore process:
- Validates the backup directory exists
- Stops all running Otto containers
- Restores SQLite data from the archive
- Restores the Redis dump file
- Restores the
.envfile
After a restore completes, you must manually restart Otto:
Backup Strategy Recommendations
- Run backups daily via cron:
0 2 * * * cd /path/to/otto && ./otto backup - Copy backup directories to off-host storage (S3, NFS, or another server)
- Test restores periodically to verify backup integrity
- Keep at least 7 days of backups
Monitoring and Health Checks
Health Endpoint
The backend exposes a health check at /api/health:
The response includes service status, deployment type, and version information.
Container Health Checks
Docker Compose configures a built-in health check for the backend container:
- Endpoint:
http://localhost:8000/api/health - Interval: 30 seconds
- Timeout: 10 seconds
- Retries: 3
You can see the health status of all containers with:
Log Monitoring
View live logs from all services:
Or target a specific service:
LangSmith Tracing (Optional)
For detailed observability into agent execution, enable LangSmith tracing through the configuration wizard (./otto configure, section 7). This sends trace data for every LLM call and tool invocation to the LangSmith platform, which is useful for debugging agent behavior and monitoring token usage.
Updating Otto
To update to the latest version:
This command performs the following steps:
- Downloads the latest CLI script, Compose files, and configuration templates from GitHub
- Sets file permissions
- Updates default skills (without overwriting user-modified skills)
- Ensures the
ottosymlink in/usr/local/binis current - Merges any new configuration options into your
.envwhile preserving your values - Pulls the latest Docker images from the container registry
- Recreates all containers with the new images
Your data is preserved across updates. The settings sync step adds new configuration options with their defaults but never removes or changes your existing values.
Pinning Image Versions
By default, Otto pulls the latest tag. To pin to a specific version, set these in your .env:
When pinned, ./otto update will still download the latest CLI and configuration files, but the Docker images will remain at the pinned version.
Security Considerations
Redis Authentication
By default, Redis runs without a password. For production, set a Redis password through the configuration wizard or directly in .env:
The CLI can auto-generate a strong random password during configuration. The password is applied to both the Redis server and all client connections automatically.
Network Exposure
In production mode, only two ports are exposed to the host:
| Port | Service | Purpose |
|---|---|---|
| 3000 | Frontend (nginx) | Web UI and API proxy |
| 8000 | Backend (FastAPI) | Direct API access |
Redis (6379) is not exposed. If you put a reverse proxy in front of Otto, you can further restrict by binding only to localhost:
In your docker-compose.yml, change the port mappings to:
API Keys and Secrets
- The
.envfile contains all secrets and is excluded from version control via.gitignore. - Never commit
.envto your repository. - The configuration wizard masks secret values when displaying them (showing only the first and last 4 characters).
- If you need to upload secrets to GitHub Actions for CI/CD, use the included helper:
./scripts/gh-secrets-upload.sh.
Container Restart Policy
All production containers are configured with restart: unless-stopped, meaning they will automatically restart after a crash or host reboot (assuming Docker is configured to start on boot).
File Permissions
The data/ directory and its subdirectories are created with default permissions. On shared servers, consider restricting access:
Troubleshooting
Services Will Not Start
Check that Docker is running and that no other services are using ports 3000 or 8000:
View container logs for error details:
If containers start but immediately exit, check for configuration errors:
Backend Reports Unhealthy
Common causes:
- Missing or invalid LLM API key -- re-run
./otto configure - Redis not reachable -- check that the Redis container is running
- Port conflict on 8000 -- stop any other service using that port
Tasks Not Processing
The ARQ worker handles task execution. Check its logs:
Common causes:
- Worker crashed and is restarting -- check for Python errors in the logs
- Redis connection lost -- verify the Redis container is healthy
- Job timeout -- the default is 30 minutes; long-running tasks may need investigation
Frontend Cannot Reach the Backend
The frontend's built-in nginx proxies API paths to the backend. If the UI loads but shows connection errors:
- Verify the backend is running:
curl http://localhost:8000/api/health - Check that both containers are on the same Docker network:
docker network ls - Look at the frontend nginx logs:
docker logs otto-frontend
Redis Connection Issues
If you set a Redis password after initial setup, make sure the password is applied consistently:
Resetting Everything
To completely reset Otto to a clean state:
This wipes all databases and state. You will need to go through the onboarding flow again.
Using a Local LLM (Ollama)
For deployments where data must not leave your network:
-
Install Ollama on the host machine:
-
Configure Otto to use it:
Select "Ollama (local)" as the LLM provider. The default URL
http://host.docker.internal:11434allows the Docker containers to reach Ollama running on the host.
Note that local models vary in quality. For best results with Ollama, use the largest model your hardware can support.