OTOBO Docker: More Secure and Performant with Hardened Docker Compose
The official RotherOSS/otobo-docker stack gets OTOBO up and running quickly — web, MariaDB, Elasticsearch, Redis, and optionally Nginx in just minutes. For production environments, "it runs" is often not enough: volume permissions, missing network isolation, root containers, and unpinned images are typical vulnerabilities that admins usually only discover under load or during a security audit.
This article summarizes how to secure and tune OTOBO Docker without bypassing the official image release cycle.
Initial Situation: What the Standard Stack Does Well — and Where It Falls Short
| Aspect | Official Stack | Typical Production Gap |
|---|---|---|
| Installation | Proven Compose snippets, .env samples | Multiple override files in COMPOSE_FILE — hard to keep track of |
| TLS | Integrated nginx override | Security headers, rate limits, and asset caching often minimal |
| Network | A single shared Docker network | DB, Redis, and Elasticsearch theoretically reachable from the proxy |
| Container Hardening | Functionally designed | Root UIDs, open volume modes (777), broad capabilities |
| Performance | Defaults | MariaDB/Redis/ES not tuned for ticket workloads |
| Operations | scripts/update.sh | No least-privilege model for technical admins |
Starting with OTOBO 11.1, OTOBO Core and OTOBO Docker have independent release cycles — a good time to intentionally own the infrastructure layer rather than just pulling images.
Two Sensible Approaches
1. Hardening Overlay (for Existing otobo-docker Installations)
If you are already using the official stack, you can retrofit a Compose override plus a host script without losing the upstream workflow:
- Non-root UIDs for web, daemon, db, redis, and Elasticsearch
- Config bind mount to
/opt/otobo-config/(read-only in the container) - Volume permissions changed from 777 to service-specific owners (e.g.,
1000:1000,999:999) - Least-privilege sudo for the
otobo-admingroup (onlydocker ps/logs,sudoediton config, no root shell)
The overlay is appended to COMPOSE_FILE and applied idempotently via script — including a backup of the Docker volumes before restarting.
2. Project-Owned Compose Stack (Greenfield / Controlled Upgrades)
For new deployments, a dedicated, streamlined Compose stack under docker/ is worth setting up:
docker-compose.yml # Base topology (db, redis, elastic, web, daemon)
docker-compose.dev.yml # HTTP on :8080, DB localhost only
docker-compose.prod.yml # Nginx TLS proxy, no direct host ports
config/mariadb/otobo.cnf # InnoDB tuning (utf8mb4, buffer pool)
config/redis/redis.conf # maxmemory + LRU eviction
config/nginx/ # TLS, security headers, static cache, rate limits
Design Principles:
| Principle | Implementation |
|---|---|
| Reproducibility | Image tags pinned in .env; upgrade = tag bump + scripts/upgrade.sh |
| Defense in Depth | cap_drop: ALL, no-new-privileges, non-root where possible |
| Network Segmentation | frontend (nginx ↔ web) and backend (datastores) — DB/Redis/ES never exposed to the proxy |
| Performance | MariaDB innodb_buffer_pool_size, Redis maxmemory, ES ES_JAVA_OPTS configured |
| Observability | Health checks with depends_on: condition: service_healthy |
| Resource Governance | CPU/memory limits per service |
In production, only nginx terminates TLS; the OTOBO web container stays internal on port 5000.
Security in Detail
Network: Frontend vs. Backend
networks:
frontend: # nginx ↔ web
backend: # web/daemon ↔ db/redis/elastic
The database and cache are not on the same network as the reverse proxy. Even if the nginx container is compromised, direct access to MariaDB remains blocked.
Container Hardening
Every service starts with:
cap_drop: [ALL]plus minimally requiredcap_addsecurity_opt: [no-new-privileges:true]- Dedicated Linux users (
mysql:mysql,redis:redis, UID mapping for OTOBO)
Nginx as the Single Entry Point
The production override sets up:
- HTTP → HTTPS redirect
- TLS 1.2/1.3 (Mozilla Intermediate Profile)
- HSTS,
X-Frame-Options,X-Content-Type-Options,Referrer-Policy - Rate limiting and connection limits on dynamic requests
- Proxy cache for
/otobo-web/assets (skins, JS, CSS) — offloads Perl/PSGI
Config Outside the Container
Config.pm and Kernel/Config/Files/ reside on the host under /opt/otobo-config/ and are mounted read-only. Changes are made via sudoedit — not via docker exec as root inside the running container.
Performance in Detail
MariaDB
innodb_buffer_pool_size≈ 70–80% of the DB container limitinnodb_log_file_size512M (fewer checkpoint churns than the 256M minimum)max_allowed_packet128M for large ticket attachmentsutf8mb4+dynamicrow format (mandatory for OTOBO)
Redis
- Pure cache:
maxmemory 512mbwithallkeys-lru - No host port — internal backend network only
Elasticsearch
bootstrap.memory_lock: true+ matchingulimits- JVM heap ≤ 50% of the container memory limit
- Host kernel:
vm.max_map_count=262144
Nginx Static Cache
Static assets are cached at the proxy for seven days (X-Cache-Status header for debugging). Dynamic ticket requests remain uncached but rate-limited.
Resource Budget (Reference Values)
| Service | CPU | RAM | Note |
|---|---|---|---|
| MariaDB | 2 | 2 GB | Keep buffer pool in sync |
| Elasticsearch | 2 | 2 GB | Heap ≤ 1 GB |
| Web (PSGI) | 2 | 2 GB | OTOBO_WEB_OPTION=deployment in prod |
| Daemon | 1 | 1 GB | GenericAgent, escalations |
| Redis | 1 | 768 MB | maxmemory < limit |
| Nginx | 1 | 256 MB | prod only |
Minimum Host: 8 GB RAM, 2 vCPUs, 50 GB SSD. For productive teams, 16 GB is recommended.
GitHub Repo: Yes — and Where?
Yes, a public repo makes sense — for three reasons:
- Reusability: Other OTOBO admins are looking for these exact patterns (hardening, nginx, tuning).
- Transparency: Security-relevant infrastructure belongs in versioned, reviewable files — not just in client projects.
- SEO & Community: Linking from the Open ITSM Hub and OTOBO documentation strengthens the ecosystem.
Suggested Repo Name: otobo-docker-production — as a community infrastructure, not as a fork of RotherOSS.
Structure:
otobo-docker-production/
├── overlay/ # For users of the official RotherOSS stack
│ ├── otobo-override-hardening.yml
│ ├── harden-docker.sh
│ └── otobo-admin.sudoers
├── compose/ # Complete project-owned stack
│ ├── docker-compose.yml
│ ├── docker-compose.dev.yml
│ ├── docker-compose.prod.yml
│ ├── config/
│ └── scripts/
└── README.md # Decision guide: Overlay vs. Compose
What Does Not Belong in the Repo
| Do Not Fork Upstream | Instead |
|---|---|
RotherOSS/otobo-docker | Reference override files, link upstream in README |
RotherOSS/otobo images | Pin tags, pull images |
Client-specific .env secrets | .env.example with placeholders |
| TLS private keys | .gitignore + cert generation script |
Upstream Contributions
Generic improvements (e.g., default volume permissions, documented non-root UIDs) can be submitted as issues or PRs to RotherOSS/otobo-docker — the overlay approach remains independent of merge speeds.
Quick Start (Compose Stack)
cd otobo-docker-production/compose
cp .env.example .env
# Set OTOBO_DB_ROOT_PASSWORD
# Development (HTTP)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# Production (TLS)
./scripts/gen-selfsigned-cert.sh helpdesk.example.com # or a real certificate
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Web installer: https://<fqdn>/otobo/installer.pl — DB host = db.
Quick Start (Hardening Overlay)
For existing /opt/otobo-docker installations:
# After cloning the overlay/ folder
bash harden-docker.sh # Volume fix, override, sudoers, restart
bash harden-docker.sh --dry-run # Preview only
bash harden-docker.sh --add-admin max.mustermann
Conclusion
The official OTOBO Docker stack is the right entry point. For production, building an intentional layer on top pays off:
- Overlay — fast, upstream-compatible, for existing installations
- Dedicated Compose stack — maximum control, clear dev/prod separation, reproducible upgrades
Both are valuable open source contributions — and belong in a dedicated repo, not in open-itsm-hub (directory/content) and not as a fork of RotherOSS.
