Features β’ Quick Start β’ Documentation β’ Screenshots β’ Contributing
Whaley is a production-ready Docker instance manager designed specifically for Capture The Flag (CTF) competitions. Deploy it on a dedicated server to provide each participant with their own isolated challenge environmentβcomplete with automatic port allocation, resource limits, and seamless CTFd integration.
| Problem | Whaley's Solution |
|---|---|
| Shared challenge instances cause interference | π Isolated containers per user/team |
| Manual port management is error-prone | π― Automatic port allocation with collision prevention |
| No visibility into player resource usage | π Real-time monitoring dashboard |
| Difficult to detect flag sharing | π Suspicious submission detection |
| Complex setup for dynamic flags | π Simple CTFd integration with auto flag injection |
|
|
- Docker Engine 24.0+ with Docker Compose v2
- 4+ CPU cores, 8GB+ RAM (see capacity planning)
- Linux server (Ubuntu 22.04+ or Debian 12+ recommended)
# Clone the repository
git clone https://github.com/jonscafe/whaley.git
cd whaley
# Configure environment
cp .env.example .env
nano .env # Edit with your settings
# Start Whaley
docker compose up -d| Interface | URL | Description |
|---|---|---|
| User Dashboard | http://your-server:8000/ |
Challenge spawning interface |
| Admin Panel | http://your-server:8000/admin |
Monitoring & management |
| API Docs | http://your-server:8000/docs |
Swagger API documentation |
# Authentication
AUTH_MODE=ctfd # "ctfd" or "none"
CTFD_URL=https://your-ctfd.com # Your CTFd instance URL
CTFD_API_KEY=ctfd_xxx... # CTFd admin API key for dynamic flags/sync
# Network
PUBLIC_HOST=auto # VPS IP ("auto" for detection)
PORT_RANGE_START=20000
PORT_RANGE_END=50000
# Admin
ADMIN_KEY=your-secure-key # Local admin key when AUTH_MODE=none
# Dynamic Flags
DYNAMIC_FLAGS_ENABLED=true
FLAG_PREFIX=FLAG # e.g., FLAG{...}
# Team Mode
TEAM_MODE=auto # "auto", "enabled", or "disabled"# PostgreSQL for high availability
DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/whaley
# Redis for distributed locking (required for multiple workers)
REDIS_URL=redis://redis:6379/0
# Network Isolation (recommended)
NETWORK_ISOLATION_ENABLED=true
NETWORK_ICC_DISABLED=true
TRUSTED_PROXIES=127.0.0.1,::1 # Only these proxies may set client IP headers
# Resource Limits (enforced on all containers)
CONTAINER_MAX_MEMORY=512m # Max memory per container
CONTAINER_MAX_CPU=1.0 # Max CPU cores per container
CONTAINER_PIDS_LIMIT=256 # Max PIDs per container (fork bomb protection)π‘ Tip: Most settings (including Authentication & CTFd integration) can be configured instantly via the Admin Panel β βοΈ Settings tab.
π Admin access: In
AUTH_MODE=ctfd, Whaley validates the submitted CTFd access token with CTFd's/api/v1/users/me, then fetches/api/v1/users/{id}and only enables/adminwhen that detailed user record hastype: "admin". InAUTH_MODE=none, admin APIs use the localADMIN_KEYfallback.
π Full configuration guide: See DOCUMENTATION.md
Create challenges in the challenges/ directory:
challenges/
βββ my-web-challenge/
βββ challenge.yaml # Challenge metadata
βββ docker-compose.yaml # Container definition
βββ Dockerfile
βββ flag.txt # Flag file (auto-injected)
βββ src/
βββ app.py
id: my-web-challenge
name: "SQL Injection Lab"
category: web
description: "Can you bypass the login?"
ports:
- 80
timeout: 3600 # 1 hourservices:
web:
build: .
ports:
- "${PORT_80}:80"
environment:
- FLAG=${FLAG}
mem_limit: 256m
cpus: 0.5
β οΈ Resource enforcement: Even if a challenge setsmem_limit: 2g, Whaley will cap it to the globalCONTAINER_MAX_MEMORY(default512m). You can set per-challenge overrides via the admin panel.
π‘οΈ Compose hardening: Whaley prepares every spawn from a per-instance copy, attaches the instance network automatically, and rejects dangerous compose options such as
privileged,network_mode, host/container namespace sharing, added capabilities/devices, Docker socket mounts, external networks/volumes, unsafe build/env file paths, symlinks, and bind mounts that escape the challenge directory.
π More examples: See DOCUMENTATION.md
Whaley supports CTFd Team Mode where instances and flags are shared per-team:
| Feature | User Mode | Team Mode |
|---|---|---|
| Instance Ownership | Per user | Per team |
| Dynamic Flags | Unique per user | Shared per team |
| Instance Control | Only spawner | Any team member |
| Suspicious Detection | User vs User | Team vs Team |
Enable with TEAM_MODE=auto to automatically detect from CTFd settings.
Concurrent Instances = Teams Γ Active Challenges Γ 0.5
RAM Required = Concurrent Instances Γ 256MB (average)
Ports Required = Concurrent Instances Γ Ports per Challenge
| Event Size | CPU | RAM | Storage | Example |
|---|---|---|---|---|
| Small (β€50 teams) | 4 cores | 8 GB | 40 GB SSD | Local CTFs |
| Medium (50-150 teams) | 8 cores | 32 GB | 100 GB SSD | University CTFs |
| Large (150-300 teams) | 16 cores | 64 GB | 200 GB SSD | National CTFs |
Scenario: 150 teams, 5 active challenges, 2 ports each
- Peak instances: 150 Γ 5 Γ 0.5 = 375 instances
- RAM needed: 375 Γ 256MB = ~96 GB (recommend 64 GB with swap)
- Ports needed: 375 Γ 2 = 750 ports
User Dashboard |
Admin Dashboard |
Event Logs |
Challenge Manager |
Dynamic Flags |
CTFd Challenge Sync |
All key Whaley settings can be changed at runtime via the Admin Panel β βοΈ Settings tab:
| Category | Settings |
|---|---|
| Instance | Timeout, max instances per user/team |
| Resources | Container max memory, CPU cores, PID limit |
| Network | Port range, isolation, public host |
| Features | Dynamic flags, flag prefix |
| Authentication | Auth mode (CTFd/None), CTFd URL, CTFd API key, local Admin Key fallback |
| Forensics | Auto capture, retention period |
Changes persist to the database and survive container restartsβno need to edit docker-compose.yaml or .env files.
Control which challenges are visible and spawnable from the Challenge Manager tab:
- π’ Active β Visible on the user dashboard, can be spawned
- π΄ Inactive β Hidden from users, spawn requests rejected (HTTP 403)
Use this during competitions to stage challenges for later rounds, or to quickly disable a broken challenge without deleting it.
Challenge uploads reject path traversal, absolute paths, and symlinks. Runtime challenge trees are also rejected if they contain symlinks. The browser editor only writes text files up to 2 MB, and Whaley blocks deleting a challenge while active instances are still using it.
Whaley enforces maximum resource limits on every container, regardless of what the challenge's docker-compose.yaml specifies:
CONTAINER_MAX_MEMORY=512m # Caps mem_limit in compose files
CONTAINER_MAX_CPU=1.0 # Caps cpus in compose files
CONTAINER_PIDS_LIMIT=256 # Injects pids_limit (fork bomb protection)
Per-challenge overrides can be set via the admin API if certain challenges need more resources.
For comprehensive documentation, see DOCUMENTATION.md:
- π§ Installation & Configuration
- π¦ Challenge Structure & Examples
- π API Reference
- π‘οΈ Security Considerations
- π Instance Forensics
- π Resource Monitoring
- π Capacity Planning
- βοΈ Admin Settings & Challenge Management
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- π Bug fixes and improvements
- π Documentation enhancements
- π¨ UI/UX improvements
- π§ͺ Test coverage
- π Internationalization
This project is licensed under the MIT License β see the LICENSE file for details.
keii Creator & Maintainer |
Built with β€οΈ for the CTF community
If you find Whaley useful, please consider giving it a β





