Docker Compose Setup for OpenClaw
Deploy OpenClaw in a containerized environment with easy updates, backups, and multi-service orchestration. This guide walks through everything from a minimal single-container setup to a production-ready stack with Redis caching and HTTPS termination.
Difficulty: Intermediate | Cost: Free | Time: ~30 minutes
Prerequisites
Before you begin, make sure you have the following installed on your host machine:
- Docker Engine 24.0+ -- verify with
docker --version - Docker Compose v2 (ships with Docker Desktop; on Linux, install the
docker-compose-pluginpackage) -- verify withdocker compose version - Basic terminal knowledge -- you should be comfortable running commands, editing files, and reading log output
- An API key from at least one supported LLM provider (Anthropic or OpenAI)
If you do not have Docker installed yet, follow the official instructions for your platform:
- macOS / Windows: Install Docker Desktop
- Ubuntu / Debian:
sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin - Fedora:
sudo dnf install docker-ce docker-ce-cli containerd.io docker-compose-plugin
After installation, confirm both tools are available:
docker --version
# Expected: Docker version 24.x or later
docker compose version
# Expected: Docker Compose version v2.x
Step 1: Project Setup
Create a dedicated directory for your OpenClaw deployment. Keeping everything in one directory makes backups and version control straightforward.
mkdir -p ~/openclaw-deploy && cd ~/openclaw-deploy
Inside this directory, create the following structure:
openclaw-deploy/
docker-compose.yml
.env
config/
soul.md # Your SOUL.md persona configuration
skills/ # Persistent skill data and custom skills
logs/ # Container log output (optional)
backups/ # Backup archives
Set up the directories:
mkdir -p config skills logs backups
If you already have a SOUL.md file, copy it into config/. Otherwise, create a starter file:
cat > config/soul.md << 'EOF'
# OpenClaw Agent Persona
You are a helpful AI assistant deployed via Docker.
Follow user instructions carefully and accurately.
EOF
Step 2: Docker Compose Configuration
Create docker-compose.yml at the root of your project directory. This file defines the OpenClaw service, its environment, storage, and resource constraints.
# docker-compose.yml
name: openclaw
services:
openclaw:
image: openclaw/openclaw:latest
container_name: openclaw-agent
restart: unless-stopped
# Environment variables -- values are pulled from the .env file
env_file:
- .env
environment:
- OPENCLAW_CONFIG_DIR=/app/config
- OPENCLAW_SKILLS_DIR=/app/skills
- OPENCLAW_LOG_LEVEL=info
# Persistent volumes
volumes:
- ./config:/app/config:ro # Mount config as read-only
- ./skills:/app/skills # Skill data persists across restarts
- ./logs:/app/logs # Persist logs outside the container
# Expose the agent API on localhost only
ports:
- "127.0.0.1:8080:8080"
# Resource limits prevent runaway usage
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "0.5"
memory: 512M
# Health check to detect a stuck or crashed process
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Key decisions in this configuration:
restart: unless-stopped-- the container restarts automatically after a crash or host reboot, but stays stopped if you explicitly stop it.127.0.0.1:8080:8080-- binds only to localhost. The agent API is not exposed to the public internet.- Resource limits -- caps at 2 CPU cores and 2 GB RAM. Adjust to fit your hardware.
- Read-only config mount -- the
:roflag on the config volume prevents the container from modifying your configuration files. - Log rotation -- the
json-filedriver withmax-sizeandmax-fileprevents logs from filling up your disk.
Step 3: Environment Variables
Create a .env file in the same directory as your docker-compose.yml. This file holds API keys and runtime configuration. Never commit this file to version control.
# .env
# --- LLM Provider API Keys ---
# At least one key is required.
ANTHROPIC_API_KEY=sk-ant-your-key-here
OPENAI_API_KEY=sk-your-openai-key-here
# --- Model Selection ---
# Choose the default model for the agent to use.
OPENCLAW_DEFAULT_MODEL=claude-sonnet-4-20250514
# Alternative options:
# claude-sonnet-4-20250514
# claude-opus-4-20250514
# gpt-4o
# gpt-4o-mini
# --- Skill Configuration ---
OPENCLAW_SKILLS_DIR=/app/skills
OPENCLAW_CONFIG_DIR=/app/config
# --- Runtime Settings ---
OPENCLAW_LOG_LEVEL=info
OPENCLAW_PORT=8080
OPENCLAW_MAX_CONCURRENT_TASKS=5
# --- Optional: Proxy / Networking ---
# HTTP_PROXY=http://proxy.example.com:8080
# HTTPS_PROXY=http://proxy.example.com:8080
# NO_PROXY=localhost,127.0.0.1
Protect the file so only your user can read it:
chmod 600 .env
Add .env to your .gitignore if this directory is under version control:
echo ".env" >> .gitignore
Step 4: Build and Run
Pull the latest image and start the container in detached mode:
docker compose pull
docker compose up -d
Verify the container is running:
docker compose ps
You should see output like:
NAME IMAGE STATUS PORTS
openclaw-agent openclaw/openclaw:latest Up 12 seconds (healthy) 127.0.0.1:8080->8080/tcp
Check the startup logs:
docker compose logs -f openclaw
Press Ctrl+C to stop following the logs. If the container starts successfully, you will see initialization messages and a line indicating the agent is listening on port 8080.
Test connectivity:
curl http://localhost:8080/health
A healthy response returns {"status":"ok"} or similar.
Step 5: Managing the Container
Start / Stop / Restart
# Stop the container (keeps the container, just stops the process)
docker compose stop
# Start a stopped container
docker compose start
# Restart (stop + start)
docker compose restart
# Tear down everything (removes containers and default network)
docker compose down
# Tear down and also remove named volumes (DESTRUCTIVE -- deletes persistent data)
docker compose down -v
Viewing Logs
# Follow live logs
docker compose logs -f openclaw
# Show the last 100 lines
docker compose logs --tail 100 openclaw
# Show logs with timestamps
docker compose logs -t openclaw
Updating to the Latest Version
Pull the newest image and recreate the container. Your data in the mounted volumes is preserved.
docker compose pull
docker compose up -d
Docker Compose will detect the newer image, stop the old container, and start a new one automatically. To pin a specific version instead of latest, change the image tag in docker-compose.yml:
image: openclaw/openclaw:1.2.3
Inspecting the Container
# Open a shell inside the running container
docker compose exec openclaw /bin/sh
# Check resource usage
docker stats openclaw-agent
Step 6: Backups
What to Back Up
| Path | Contents | Priority |
|---|---|---|
.env | API keys, configuration | Critical |
config/ | SOUL.md and agent persona files | Critical |
skills/ | Installed skills and custom skill data | High |
docker-compose.yml | Service definitions | Medium (can be recreated) |
Manual Backup
tar -czf backups/openclaw-backup-$(date +%Y%m%d-%H%M%S).tar.gz \
.env \
docker-compose.yml \
config/ \
skills/
Automated Backup Script
Save the following as backup.sh in your project directory:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="./backups"
RETAIN_DAYS=30
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
ARCHIVE="${BACKUP_DIR}/openclaw-backup-${TIMESTAMP}.tar.gz"
mkdir -p "${BACKUP_DIR}"
# Create the backup archive
tar -czf "${ARCHIVE}" \
.env \
docker-compose.yml \
config/ \
skills/
echo "Backup created: ${ARCHIVE}"
# Remove backups older than the retention period
find "${BACKUP_DIR}" -name "openclaw-backup-*.tar.gz" -mtime +${RETAIN_DAYS} -delete
echo "Cleaned up backups older than ${RETAIN_DAYS} days."
Make it executable and schedule it with cron:
chmod +x backup.sh
# Run daily at 2:00 AM
crontab -e
# Add the following line (adjust the path to match your setup):
# 0 2 * * * cd /home/youruser/openclaw-deploy && ./backup.sh >> logs/backup.log 2>&1
Step 7: Multi-Service Setup
For production workloads, you may want to add Redis for caching and a reverse proxy for HTTPS. Extend your docker-compose.yml with additional services.
Adding Redis for Caching
# docker-compose.yml (extended)
name: openclaw
services:
openclaw:
image: openclaw/openclaw:latest
container_name: openclaw-agent
restart: unless-stopped
env_file:
- .env
environment:
- OPENCLAW_CONFIG_DIR=/app/config
- OPENCLAW_SKILLS_DIR=/app/skills
- OPENCLAW_LOG_LEVEL=info
- REDIS_URL=redis://redis:6379
volumes:
- ./config:/app/config:ro
- ./skills:/app/skills
- ./logs:/app/logs
ports:
- "127.0.0.1:8080:8080"
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
redis:
image: redis:7-alpine
container_name: openclaw-redis
restart: unless-stopped
volumes:
- redis-data:/data
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
volumes:
redis-data:
Redis is configured with a 256 MB memory cap and an LRU eviction policy, so it acts purely as a cache. The depends_on condition ensures OpenClaw waits for Redis to be healthy before starting.
Adding a Reverse Proxy with Caddy (Automatic HTTPS)
Caddy automatically provisions and renews TLS certificates via Let's Encrypt. Add it as a service and point it at the OpenClaw container.
Create Caddyfile in your project directory:
yourdomain.com {
reverse_proxy openclaw:8080
encode gzip
log {
output file /data/access.log
}
}
Then add Caddy to docker-compose.yml:
caddy:
image: caddy:2-alpine
container_name: openclaw-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 (QUIC)
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- openclaw
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
Add the Caddy volumes to the top-level volumes block:
volumes:
redis-data:
caddy-data:
caddy-config:
When using Caddy as the public-facing proxy, remove the ports mapping from the OpenClaw service entirely. Traffic flows: Internet -> Caddy (ports 80/443) -> OpenClaw (internal port 8080). The OpenClaw port no longer needs to be published.
If you prefer nginx instead of Caddy, replace the Caddy service with:
nginx:
image: nginx:1.27-alpine
container_name: openclaw-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- openclaw
With nginx you must manage TLS certificates yourself (for example, with certbot or an ACME sidecar container). For most deployments, Caddy is the simpler choice.
Security Notes
-
Bind ports to localhost only. The default configuration uses
127.0.0.1:8080:8080. Never bind to0.0.0.0unless the port is behind a reverse proxy or firewall. -
Use Docker secrets for API keys in production. Instead of storing keys in
.env, define them as secrets:# docker-compose.yml services: openclaw: secrets: - anthropic_api_key - openai_api_key environment: - ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key - OPENAI_API_KEY_FILE=/run/secrets/openai_api_key secrets: anthropic_api_key: file: ./secrets/anthropic_api_key.txt openai_api_key: file: ./secrets/openai_api_key.txtCreate the secrets directory and files:
mkdir -p secrets echo "sk-ant-your-key-here" > secrets/anthropic_api_key.txt echo "sk-your-openai-key-here" > secrets/openai_api_key.txt chmod 600 secrets/*.txtSecrets are mounted as files inside the container at
/run/secrets/and never appear indocker inspectoutput or environment variable listings. -
Use read-only filesystems where possible. Add
read_only: trueto the service definition and explicitly mount writable paths withtmpfsor named volumes:services: openclaw: read_only: true tmpfs: - /tmp volumes: - ./config:/app/config:ro - ./skills:/app/skills - ./logs:/app/logs -
Drop unnecessary Linux capabilities. Reduce the container's attack surface:
services: openclaw: cap_drop: - ALL cap_add: - NET_BIND_SERVICE -
Run as a non-root user. If the image supports it, set the user explicitly:
services: openclaw: user: "1000:1000" -
Keep images updated. Regularly pull new images to get security patches:
docker compose pull && docker compose up -d.
Troubleshooting
Port Conflict
Symptom: Error: bind: address already in use
Another process is using port 8080. Find it and either stop it or change the OpenClaw port.
# Find what is using port 8080
lsof -i :8080
# or
ss -tlnp | grep 8080
To change the OpenClaw port, edit the ports mapping in docker-compose.yml:
ports:
- "127.0.0.1:9090:8080" # Host port 9090 -> container port 8080
Permission Denied on Volumes
Symptom: The container logs show Permission denied when reading config or writing to skills/logs.
This happens when the container runs as a different UID than the host directory owner. Fix it by matching ownership:
# Check the UID the container runs as (common: 1000 or 65534)
docker compose exec openclaw id
# Set ownership on the host to match
sudo chown -R 1000:1000 config/ skills/ logs/
Container Keeps Restarting
Symptom: docker compose ps shows status Restarting in a loop.
Check the logs for the root cause:
docker compose logs --tail 50 openclaw
Common causes:
- Missing or invalid API key -- verify your
.envfile has a validANTHROPIC_API_KEYorOPENAI_API_KEY. - Configuration syntax error -- check
config/soul.mdfor malformed content. - Insufficient memory -- if the container is being OOM-killed, increase the memory limit in
deploy.resources.limits.memory.
Container Starts but Health Check Fails
Symptom: Status shows (health: starting) and then (unhealthy).
The health check endpoint may not be ready yet. Increase the start_period:
healthcheck:
start_period: 30s # Give the app more time to initialize
If /health is not the correct endpoint for your version, check the OpenClaw documentation and update the test command accordingly.
Cannot Connect to Redis
Symptom: OpenClaw logs show Error: connect ECONNREFUSED 127.0.0.1:6379.
The REDIS_URL should reference the Docker Compose service name, not localhost:
# Wrong
REDIS_URL=redis://localhost:6379
# Correct
REDIS_URL=redis://redis:6379
Inside the Docker network, services communicate by service name, not by localhost.
Disk Space Running Out
Symptom: Container crashes or Docker commands fail with no space left on device.
Prune unused Docker resources:
# Remove stopped containers, dangling images, and build cache
docker system prune -f
# Also remove unused volumes (careful -- this deletes data in unnamed volumes)
docker system prune --volumes -f
Check your backup directory as well and remove old archives if retention is not configured.
Quick Reference
| Task | Command |
|---|---|
| Start services | docker compose up -d |
| Stop services | docker compose stop |
| Restart services | docker compose restart |
| View live logs | docker compose logs -f openclaw |
| Update to latest | docker compose pull && docker compose up -d |
| Check status | docker compose ps |
| Open a shell | docker compose exec openclaw /bin/sh |
| Resource usage | docker stats openclaw-agent |
| Full teardown | docker compose down |
| Manual backup | tar -czf backup.tar.gz .env docker-compose.yml config/ skills/ |
Guide maintained by the OpenClaw community. Last updated: February 2026.