Contributing

All docs

Contributions are welcome. The canonical, always-current details live in the repository — this page points you to them.

Where to start

Reporting a vulnerability

Please do not open a public issue for security problems. Follow the private disclosure process in SECURITY.md.

Local development environment

The shipped compose.yaml is the development stack — nginx + PHP-FPM + PostgreSQL — with common tasks wrapped in a justfile:

git clone https://github.com/ubermuda/qualendar.git
cd qualendar
docker compose up -d          # start containers
composer install              # install PHP dependencies
just migrate-run              # run database migrations

Its services attach to an external traefik network and request certificates from a step-ca resolver — which is what makes https://<project>.dev.localhost resolve with a trusted certificate. That proxy is a separate stack; it is not created by docker compose up here. (For a production deployment you don't need any of it — see Self-hosting.)

The Traefik + step-ca proxy

Stand it up once on the shared traefik network. step-ca is a local ACME certificate authority, so Traefik issues trusted certificates for *.dev.localhost automatically on first request. Put this in a separate directory (e.g. proxy/compose.yml):

services:
  dnsmasq:                       # resolves *.dev.localhost inside the network
    image: jpillora/dnsmasq:1.1.0
    restart: unless-stopped
    cap_add: [NET_ADMIN]
    volumes:
      - ./dnsmasq.conf:/etc/dnsmasq.conf:ro
    networks:
      traefik: { ipv4_address: 172.20.0.3 }

  step-ca:                       # local ACME certificate authority
    image: smallstep/step-ca:0.30.2
    restart: unless-stopped
    dns: ["172.20.0.3"]
    environment:
      DOCKER_STEPCA_INIT_NAME: "Local Dev CA"
      DOCKER_STEPCA_INIT_DNS_NAMES: "step-ca,localhost"
      DOCKER_STEPCA_INIT_ACME: "true"
      DOCKER_STEPCA_INIT_PASSWORD_FILE: "/home/step/secrets/password"
      PWDPATH: "/home/step/secrets/password"
    volumes:
      - ./step-ca:/home/step
    healthcheck:
      test: ["CMD", "wget", "-qO-", "--no-check-certificate", "https://localhost:9000/health"]
      interval: 5s
      retries: 20
      start_period: 15s
    networks:
      traefik: { ipv4_address: 172.20.0.4 }

  traefik:
    image: traefik:v3
    restart: unless-stopped
    depends_on:
      step-ca: { condition: service_healthy }
    ports: ["80:80", "443:443", "5432:5432"]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./certs:/certs
      - ./step-ca/certs/root_ca.crt:/root_ca.crt:ro
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.docker.network=traefik
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.postgres.address=:5432
      - --certificatesresolvers.stepca.acme.caServer=https://step-ca:9000/acme/acme/directory
      - --certificatesresolvers.stepca.acme.storage=/certs/acme.json
      - --certificatesresolvers.stepca.acme.tlsChallenge=true
      - --certificatesresolvers.stepca.acme.cacertificates=/root_ca.crt
    networks:
      traefik: { ipv4_address: 172.20.0.2 }

networks:
  traefik:
    external: true

Alongside it, a dnsmasq.conf that points every *.dev.localhost name at the Traefik container, so step-ca can reach it to validate certificates:

no-resolv
no-hosts
server=127.0.0.11
address=/dev.localhost/172.20.0.2

Then, once:

# the shared network — the static IPs above need this subnet
docker network create --subnet=172.20.0.0/24 traefik

# a password to protect step-ca's keys
mkdir -p step-ca/secrets
openssl rand -base64 32 > step-ca/secrets/password

docker compose up -d

# trust the CA so browsers accept the certificates (macOS example)
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain \
  step-ca/certs/root_ca.crt

With that running, the app's own docker compose up joins the same network and https://<project>.dev.localhost works with a trusted certificate.