Quickstart — Deploy a CTF in ~5 minutes¶
This guide gets a working CTFHive instance running on your local machine with a real challenge imported and playable. All commands run from the repo root unless noted otherwise.
Prerequisites
- Python 3.13+ and
uvinstalled - Git clone of this repo
- (Path B only) Docker and Docker Compose installed
Choose your path¶
1. Install dependencies
2. Create a .env file
Copy the example and change the three secrets. The app reads .env automatically via python-dotenv.
Open .env and set at minimum:
SECRET_KEY=<random-32+-char-string>
ADMIN_KEY=<random-32+-char-string>
ENCRYPTION_KEY=<random-32-char-string>
# Optional — change if you want a different prefix on flags
DEFAULT_FLAG_PREFIX=GRIZZ
Never use the defaults in production
The shipped defaults (change-me-in-production, change-me-admin-key, change-me-encryption-key) will cause validate_production_runtime to raise a RuntimeError when APP_ENV=production. Change them before any public deployment.
3. Bootstrap the database
This creates all tables. On first run the app also seeds a default admin account from ADMIN_USERNAME / ADMIN_PASSWORD (see Configuration).
4. Start the development server
This runs flask run --debug --port 5000. Open http://localhost:5000 in your browser.
5. Log in as admin
Username: value of ADMIN_USERNAME (default: admin)
Password: value of ADMIN_PASSWORD (default: GrizzAdmin!2025)
Default admin password
The ADMIN_PASSWORD default is GrizzAdmin!2025 (in ctfapp/config.py). The example .env in ctfapp/.env shows GrizzAdmin (no suffix). Your effective password is whichever value is in your .env file. Change it before any public deployment.
1. Create a .env file at the repo root
Change the three secrets in .env:
SECRET_KEY=<random-32+-char-string>
ADMIN_KEY=<random-32+-char-string>
ENCRYPTION_KEY=<random-32-char-string>
2. Start the full stack
This brings up three containers via docker-compose.yml:
| Container | Image | Purpose |
|---|---|---|
grizzhacks-ctf |
built from Dockerfile |
Gunicorn Flask app on port 5000 (localhost-only) |
grizzhacks-redis |
redis:7-alpine |
Cache, rate limiting |
grizzhacks-nginx |
nginx:1.27-alpine |
Reverse proxy on ports 80 / 443 |
The app is accessible at http://localhost (nginx proxies to the Flask container).
TLS in Docker Compose
The compose file includes nginx with Let's Encrypt volume mounts (certbot/conf, certbot/www). For local use, HTTP on port 80 is sufficient. HTTPS configuration is covered in Deploy with Docker Compose.
3. Wait for healthy containers
The ctf-app container has a health check (curl -f http://localhost:5000/). The nginx container starts only after ctf-app is healthy.
4. Log in
Open http://localhost and sign in with the admin credentials from your .env.
Import an example challenge¶
The repo ships example challenges under challenges/. Import one with the CLI:
The importer reads the YAML manifest, creates the challenge in the database, and copies associated files. For a full description of the YAML schema see Challenge format.
Validate before importing
This parses and validates the manifest without touching the database.
View the challenge and submit a flag¶
- Navigate to http://localhost:5000/challenges (or http://localhost for Docker).
- Click Circular — the challenge you just imported.
- The challenge is marked
is_dynamic: false, so there is no container to spawn; download the provided files and solve the puzzle.
What does a derived flag look like?¶
For static challenges the flag is derived at import time. The format is:
For example, with DEFAULT_FLAG_PREFIX=GRIZZ:
The derivation uses ADMIN_KEY as the HMAC key and the team's secret plus the challenge ID as the message. Changing ADMIN_KEY invalidates all derived flags — see Configuration for details.
What's next?¶
- Installation — full prerequisites, Makefile targets, optional Redis setup
- Configuration — complete environment variable reference
- Challenge format — write your own challenge YAML
- Importing challenges — bulk import workflows
- Admin panel — managing users, teams, and scoring