Deploy with Docker Compose
This guide runs curral on a server, serving a Cloudflare R2 Data Catalog, behind Caddy with automatic HTTPS from Let’s Encrypt.
client ──HTTPS:443──▶ caddy ──HTTP:8080 (internal network)──▶ curral ──▶ R2 Data CatalogOnly Caddy publishes ports. curral is reachable only on the Compose network.
Prerequisites
Section titled “Prerequisites”- Docker from Docker’s repository (
get.docker.com). The snap Docker breaks container DNS, port publishing and running the binary. Check withwhich docker: it must be/usr/bin/docker. - A domain with an A/AAAA record pointing to the server. On Cloudflare, leave the proxy off (grey cloud) so Let’s Encrypt can validate.
- Ports 80 and 443 open for inbound traffic.
- An R2 bucket with Data Catalog enabled, and an API token with R2 Data Catalog and R2 Storage permissions.
Layout
Section titled “Layout”Directorycurral/
- .env credentials and domain (chmod 600)
- docker-compose.yml
- Caddyfile
Directoryconfig/ mounted read-only at /etc/curral
- catalog.yaml
- users.yaml
- policy.rego
-
Environment. The values are in the bucket’s Data Catalog settings.
.env CURRAL_DOMAIN=curral.example.comR2_CATALOG_URI=https://catalog.cloudflarestorage.com/<account_id>/<bucket>R2_WAREHOUSE=<account_id>_<bucket>R2_TOKEN=<cloudflare-api-token>R2_SCHEMA=<namespace>R2_ALLOWED_PATH=s3://<bucket>/R2_CACHE_TTL=30s -
Catalog.
config/catalog.yaml extensions: [httpfs, avro, iceberg]secrets:- name: r2_catalogtype: icebergparams:TOKEN: ${R2_TOKEN}databases:- name: lakepath: ${R2_WAREHOUSE}schema: ${R2_SCHEMA:-default}cache_ttl: ${R2_CACHE_TTL:-0s}options:TYPE: icebergSECRET: r2_catalogENDPOINT: ${R2_CATALOG_URI}default: lake -
Admin user. Generate a strong password and its hash:
Terminal window openssl rand -base64 24docker run --rm -it lucasapassos/curral:v0.4.1 hash-passwordconfig/users.yaml users:- name: adminpassword_hash: "<bcrypt hash>"roles: [admin] -
Policy. Start with an admin who can do anything, then add roles.
config/policy.rego package curralimport rego.v1default allow := falseallow if "admin" in input.rolesThe container runs as uid 65532, which must be able to read the files:
Terminal window sudo chown -R 65532:65532 config && sudo chmod 600 config/* -
Caddy.
Caddyfile {$CURRAL_DOMAIN} {reverse_proxy curral:8080header {Strict-Transport-Security "max-age=31536000"-Server}} -
Compose.
docker-compose.yml services:curral:image: lucasapassos/curral:v0.4.1environment:CURRAL_CATALOG: /etc/curral/catalog.yamlCURRAL_USERS: /etc/curral/users.yamlCURRAL_POLICY: /etc/curral/policy.regoCURRAL_ALLOWED_PATH: ${R2_ALLOWED_PATH:?}CURRAL_MAX_CONCURRENCY: 8CURRAL_MEMORY_LIMIT: 2GBCURRAL_QUERY_TIMEOUT: 600sCURRAL_AUDIT_LOG: /var/log/curral/audit.jsonlCURRAL_TRUSTED_PROXY: 172.31.247.10 # only Caddy may set X-Forwarded-ForR2_CATALOG_URI: ${R2_CATALOG_URI:?}R2_WAREHOUSE: ${R2_WAREHOUSE:?}R2_TOKEN: ${R2_TOKEN:?}R2_SCHEMA: ${R2_SCHEMA:?}R2_CACHE_TTL: ${R2_CACHE_TTL:-30s}volumes:- ./config:/etc/curral:ro- audit:/var/log/curralread_only: truetmpfs:- /var/lib/curral/tmp:uid=65532,gid=65532,mode=0700cap_drop: [ALL]security_opt: ["no-new-privileges:true"]mem_limit: 3grestart: unless-stoppednetworks: [curral]caddy:image: caddy:2ports: ["80:80", "443:443"]environment:CURRAL_DOMAIN: ${CURRAL_DOMAIN:?}volumes:- ./Caddyfile:/etc/caddy/Caddyfile:ro- caddy_data:/datadepends_on: [curral]restart: unless-stoppednetworks:curral:ipv4_address: 172.31.247.10networks:curral:ipam:config:- subnet: 172.31.247.0/24volumes:audit:caddy_data:Caddy has a fixed IP so curral can trust
X-Forwarded-Forfrom it alone. Never trust the whole subnet: it includes the Docker gateway. -
Start and test.
Terminal window docker compose up -ddocker compose logs -f caddy # wait for "certificate obtained successfully"From your machine (curl prompts for the password):
Terminal window curl -u admin https://curral.example.com/v1/query \-d '{"sql":"SELECT count(*) FROM <table>","format":"csv"}'
Day-to-day
Section titled “Day-to-day”| Task | Command |
|---|---|
| Edit users or the policy | edit config/*, then docker compose kill -s HUP curral |
Change the catalog or .env |
docker compose up -d |
| Upgrade | change the image tag, then docker compose up -d |
| Read the audit log | docker run --rm -v curral_audit:/a alpine tail /a/audit.jsonl |
The image has no shell, so docker compose exec does not work.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause |
|---|---|
exec /usr/local/bin/curral: operation not permitted |
snap Docker; install Docker from get.docker.com |
rego_parse_error: package expected |
a copied ``` fence on the first line; check head -2 config/* |
Caddy 502, lookup curral ... server misbehaving |
curral is not running; check docker compose logs curral |
| No certificate | DNS not pointing to the server, ports closed, or Cloudflare proxy on |
| External access error reading a table | R2_ALLOWED_PATH does not cover the files’ path |
Permission denied reading config/ |
run the chown 65532:65532 from step 4 |
| Port 80 already allocated | another proxy runs on the host; remove the caddy service and point that proxy at 127.0.0.1:8080 |