Docker Deployment
Deploy Gmail Secretary MCP with Docker for persistent email caching and reliable operation.
Prerequisites
- Docker and Docker Compose installed
- Google Cloud OAuth credentials (client ID + secret) or Gmail App Password
- Basic familiarity with YAML configuration
Quick Start
1. Clone and configure:
git clone https://github.com/johnneerdael/gmail-secretary-map.git
cd gmail-secretary-map
# Create config directory
mkdir -p config
# Copy sample config
cp config.sample.yaml config/config.yaml2. Generate a secure bearer token:
uuidgenuuidgen # or: openssl rand -hex 32[guid]::NewGuid().ToString()Add to config/config.yaml:
bearer_auth:
enabled: true
token: "your-generated-uuid-here"3. Start the container:
docker compose up -d4. Run authentication setup:
# Option 1: OAuth2 (recommended)
docker exec -it workspace-secretary uv run python -m workspace_secretary.auth_setup \
--client-id='YOUR_CLIENT_ID.apps.googleusercontent.com' \
--client-secret='YOUR_CLIENT_SECRET'
# Option 2: App Password (no Google Cloud project needed)
docker exec -it workspace-secretary uv run python -m workspace_secretary.app_passwordSimplified Auth (v4.2.2+)
Tokens automatically save to /app/config/token.json. No --token-output flag needed.
5. Monitor sync progress:
docker compose logs -fYou should see:
INFO - Synced 50 new emails from INBOX
INFO - Embedding 50 emails from INBOXDatabase Backends
SQLite (Default)
Zero configuration, perfect for single-user deployments:
database:
backend: sqlite
path: /app/config/email_cache.dbPostgreSQL (Recommended for Production)
Required for semantic search with pgvector:
database:
backend: postgres
host: postgres
port: 5432
name: secretary
user: secretary
password: ${POSTGRES_PASSWORD}
embeddings:
enabled: true
model: text-embedding-3-small
dimensions: 1536Docker Compose with PostgreSQL:
# docker-compose.yml
services:
workspace-secretary:
image: ghcr.io/johnneerdael/gmail-secretary-map:latest
ports:
- "8000:8000" # MCP server
- "8080:8080" # Web UI
volumes:
- ./config:/app/config
environment:
- POSTGRES_PASSWORD=your-secure-password
- OPENAI_API_KEY=sk-... # For embeddings
- ENGINE_API_URL=http://127.0.0.1:8001
depends_on:
postgres:
condition: service_healthy
postgres:
image: pgvector/pgvector:pg16
environment:
- POSTGRES_USER=secretary
- POSTGRES_PASSWORD=your-secure-password
- POSTGRES_DB=secretary
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U secretary"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:See Semantic Search for embedding configuration details.
Volume Mounts
The container requires a single volume mount:
| Host Path | Container Path | Purpose |
|---|---|---|
./config/ | /app/config/ | Configuration, tokens, and cache |
volumes:
- ./config:/app/configYour config/ folder contains:
config.yaml- Configuration filetoken.json- OAuth tokens (created by auth setup)email_cache.db- SQLite cache (if using SQLite backend)
Single folder mount only
# ✅ Correct
volumes:
- ./config:/app/config
# ❌ Wrong - conflicting mounts
volumes:
- ./config.yaml:/app/config/config.yaml:ro
- ./config:/app/configSync Behavior
Initial Sync
On first startup:
- Connects to Gmail IMAP with CONDSTORE support
- Downloads email metadata and bodies in batches
- Stores in database (SQLite or PostgreSQL)
- Generates embeddings if enabled
Sync times by mailbox size:
| Emails | Time |
|---|---|
| ~1,000 | 1-2 minutes |
| ~10,000 | 5-10 minutes |
| ~25,000 | 15-30 minutes |
Incremental Sync
After initial sync:
- IDLE push notifications for real-time updates
- CONDSTORE for efficient flag change detection
- UIDNEXT tracking for new message detection
- Typical incremental sync: < 1 second
Cache Management
Reset the cache:
docker compose stop
rm config/email_cache.db # SQLite only
docker compose startView sync stats (SQLite):
docker exec workspace-secretary sqlite3 /app/config/email_cache.db \
"SELECT folder, COUNT(*) as emails FROM emails GROUP BY folder;"Authentication
OAuth2 Setup
Run inside the container after it's started:
docker exec -it workspace-secretary uv run python -m workspace_secretary.auth_setup \
--client-id='YOUR_CLIENT_ID.apps.googleusercontent.com' \
--client-secret='YOUR_CLIENT_SECRET'The manual OAuth flow:
- Open the printed authorization URL in your browser
- Login and approve access
- Copy the full redirect URL (even if page doesn't load)
- Paste when prompted
- Tokens saved automatically to
/app/config/token.json
App Password Setup
Alternative without Google Cloud project:
docker exec -it workspace-secretary uv run python -m workspace_secretary.app_passwordEnter your Gmail address and App Password when prompted.
Token Refresh
Tokens auto-refresh. If refresh fails, re-run auth setup and restart:
docker exec -it workspace-secretary uv run python -m workspace_secretary.auth_setup \
--client-id='...' --client-secret='...'
docker compose restartEnvironment Variables
| Variable | Purpose | Default |
|---|---|---|
POSTGRES_PASSWORD | PostgreSQL password | Required for postgres backend |
OPENAI_API_KEY | Embeddings API key | Required if embeddings enabled |
WORKSPACE_TIMEZONE | IANA timezone | From config.yaml |
LOG_LEVEL | Logging verbosity | INFO |
Production Recommendations
Resource Limits
services:
workspace-secretary:
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256MRestart Policy
services:
workspace-secretary:
restart: alwaysLog Rotation
services:
workspace-secretary:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"Connecting Clients
The server exposes a Streamable HTTP endpoint at:
http://localhost:8000/mcpWith bearer auth header:
Authorization: Bearer your-generated-uuid-hereSee the Client Setup Guide for Claude Desktop, VS Code, Cursor, and other MCP clients.
Port Configuration
The Docker container exposes three services:
| Port | Service | Purpose |
|---|---|---|
| 8000 | MCP Server | AI client connections |
| 8001 | Engine API | Internal only (not exposed) |
| 8080 | Web UI | Human interface |
Port mapping in docker-compose.yml:
ports:
- "8000:8000" # MCP server
- "8080:8080" # Web UI
# Note: 8001 is internal only, do not exposeTroubleshooting
"Database not initialized" error
Upgrade to v4.2.2+ which fixes this issue.
"Missing client_id or client_secret"
Re-run auth setup with v4.2.1+ which saves credentials in token.json.
PostgreSQL connection refused
Ensure postgres service is healthy before secretary starts:
depends_on:
postgres:
condition: service_healthySync appears stuck
docker compose logs --tail=100 | grep -i errorCommon causes:
- Invalid OAuth tokens (re-run auth setup)
- Network connectivity issues
- Gmail rate limiting
High memory during initial sync
Large mailboxes use more memory during sync. Increase container memory limits if needed.
Next: Configure Semantic Search for AI-powered email discovery.