Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## vX.X.X (YYYY-MM-DD)

### Added
- Auto-detect a standalone Docker Compose file (`compose.yaml`, `compose.yml`, `docker-compose.yaml`, or `docker-compose.yml`) next to `challenge.yml` for multi-service challenges, so authors no longer have to embed the whole document as an inline `compose_definition` string
- Optional `deploy_parameters.compose_file` to point at a differently named compose file (relative to the challenge directory)
- Default `playbook_name` to `custom_compose` when a compose file is detected and no playbook is set explicitly
- `deploy_parameters.expose` block for compose challenges: automatically wire Traefik (http: external network + router/service labels + domain) and published ports (tcp: reuses per-team host-port randomization/persistence), removing the need to hand-write Traefik labels, attach networks, or pick host ports. Multiple HTTP exposures get per-service subdomains, and connection info is derived automatically
- Validate `expose` entries at challenge index time (structure + that referenced services exist in the compose definition)
- Document `traefik_network` in `config.example.yaml` (required for http challenges and http exposures)

### Changed
- `example/custom_compose` now demonstrates the standalone `docker-compose.yml` workflow with an `expose` block; inline `compose_definition` still works and takes precedence over a sibling file

## v0.7.0 (2026-03-18)

### Added
Expand Down
83 changes: 81 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ All endpoints except `/health` require a JWT bearer token. Tokens are generated

## Challenge Definitions

Example challenge file:
Example challenge files (one per playbook type):

- `data/challenges/example/challenge.yml`
- `data/challenges/example/http/challenge.yml`
- `data/challenges/example/tcp/challenge.yml`
- `data/challenges/example/custom_compose/challenge.yml` (with a standalone `docker-compose.yml`)

Key fields:

Expand All @@ -101,6 +103,83 @@ Key fields:
- `deploy_parameters`: image, ports, env, or Compose definition
- `flags`, `value`, `description`, `tags`

### Multi-service / Docker Compose challenges

For challenges that need more than one container (databases, sidecars, custom
networks/volumes), drop a standard Docker Compose file next to your
`challenge.yml`:

```
data/challenges/web/my-chall/
├── challenge.yml
└── docker-compose.yml
```

Galvanize auto-detects the first of `compose.yaml`, `compose.yml`,
`docker-compose.yaml`, or `docker-compose.yml` in the challenge directory,
validates it, and deploys it. When a compose file is present, `playbook_name`
defaults to `custom_compose`, so a minimal `challenge.yml` is enough:

```yaml
name: my-chall
author: YourName
category: web
type: zync

deploy_parameters:
unique: false
```

This lets you develop and test the stack locally with a plain
`docker compose up`, then ship the file unchanged.

#### Exposing services (`expose`)

Compose challenges no longer need hand-written Traefik labels, manual network
wiring, or hand-picked host ports. Declare an `expose` block and Galvanize wires
the networking for you — the same automation the `http`/`tcp` playbooks provide
for single containers:

```yaml
deploy_parameters:
unique: false
expose:
- service: web # HTTP via Traefik → auto domain + SSL
port: 80
type: http
- service: ssh # raw TCP → published host port
port: 22
type: tcp
scheme: ssh # optional, only affects the rendered connection URL
```

For each entry Galvanize will, on deploy:

- `type: http` — attach the service to the external Traefik network, add the
`traefik.enable` + router/service labels, and route
`https://<project>.<instancer_host>/` to it (when more than one HTTP service
is exposed, each gets a `<service>-<project>.<instancer_host>` subdomain).
Requires `traefik_network` in `instancer.extra_deployment_parameters`.
- `type: tcp` — publish the container port. With
`instancer.randomize_published_ports` enabled, the host port is randomized per
team and persisted, so instances don't collide.

Connection info is returned automatically from the resulting Traefik route or
published port.

Notes:

- Use `deploy_parameters.compose_file: <name>` to point at a differently named
file (relative to the challenge directory).
- An inline `deploy_parameters.compose_definition` string still works and takes
precedence over any compose file, so existing challenges are unaffected.
- `expose` is optional: if you prefer, you can still hand-write Traefik labels
and `ports:` in the compose file and omit `expose` entirely.
- The project name is generated automatically per team; do not set
`container_name` in services or instances will collide.
- Image `build:` contexts and host bind mounts reference paths on the **target
deploy host**, not the instancer — prefer pre-built images.

## Troubleshooting

- If deployments fail, verify SSH access to the target host and that Docker is installed.
Expand Down
3 changes: 3 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ instancer:
memory: "512M" # Memory limit per container (e.g. "256M", "512M", "1G")
pids_limit: 256 # Max number of PIDs per container (fork bomb protection)
extra_deployment_parameters: # Additional parameters for deployment (added to vars in Ansible)
# Name of the external Docker network Traefik watches on the target host(s).
# Required for http challenges and for http exposures in compose challenges.
traefik_network: "traefik"
example_param: "example_value"
65 changes: 43 additions & 22 deletions data/challenges/example/custom_compose/challenge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,50 @@ category: web

type: zync

# The 'custom_compose' playbook deploys an arbitrary Docker Compose definition.
# Use this when a challenge needs multiple services, custom networks, or volumes.
# Resource limits are applied to every service in the definition.
playbook_name: custom_compose
# Multi-service challenges just need a standard Docker Compose file next to
# this challenge.yml. Galvanize auto-detects compose.yaml, compose.yml,
# docker-compose.yaml, or docker-compose.yml and deploys it for you.
#
# Because a compose file is present, playbook_name defaults to
# "custom_compose" and can be omitted. You can still set it explicitly:
# playbook_name: custom_compose
#
# This means you can develop and test the stack locally with a plain
# `docker compose up`, then drop the file in here unchanged.

deploy_parameters:
unique: false

# Inline Docker Compose definition.
# The project name is generated automatically.
compose_definition: |-
services:
web:
image: nginx:alpine
ports:
- "80:80"
environment:
FLAG: "flag{example_custom}"
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
# 'expose' lets Galvanize do the networking wiring for you, just like the
# http/tcp playbooks do for single-container challenges. Each entry names a
# service from the compose file, its container port, and how to reach it:
#
# type: http -> routed through Traefik (auto domain + SSL). Requires
# 'traefik_network' in extra_deployment_parameters.
# type: tcp -> published as a host port. With
# instancer.randomize_published_ports enabled, the host port
# is randomized per team to avoid collisions.
#
# You no longer need to hand-write Traefik labels, attach the external
# network, or pick host ports yourself.
expose:
- service: web
port: 80
type: http
# - service: ssh
# port: 22
# type: tcp
# scheme: ssh # optional, used only to render nicer connection URLs

# By default the sibling compose file is used automatically. To point at a
# differently named file (relative to this directory), set:
# compose_file: stack.yaml
#
# Alternatively, you can still inline the whole definition as a string:
# compose_definition: |-
# services:
# web:
# image: nginx:alpine

# Override global default_resource_limits for this challenge.
# Applied to every service in the compose definition.
Expand All @@ -39,8 +59,9 @@ deploy_parameters:
value: 7

description: |-
Example custom compose challenge. Multiple services are deployed using
an inline Docker Compose definition.
Example custom compose challenge. Multiple services are deployed from a
standalone docker-compose.yml file, and the web service is exposed over HTTP
via Traefik using the 'expose' block.

flags:
- flag{example_custom}
Expand Down
11 changes: 11 additions & 0 deletions data/challenges/example/custom_compose/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
web:
image: nginx:alpine
environment:
FLAG: "flag{example_custom}"
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
Loading
Loading