Ctrl K
ComunidadGuías…

docker-compose.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# The dev stack holds real data on some installs, and the django container parses
# uploaded files with Ghostscript and Pillow. Without root or capabilities, a
# compromised parser cannot take ownership of files or use root's reach over the
# bind mounts. Still within its reach: everything this UID owns on those mounts,
# including ./app, and the host's loopback services (network_mode: host).
x-unprivileged: &unprivileged
  user: "${MY_PRACTICE_UID:-1000}:${MY_PRACTICE_GID:-1000}"
  cap_drop:
    - ALL
  security_opt:
    - no-new-privileges:true

services:
  postgres:
    image: postgres:17-alpine
    container_name: my-practice-db
    restart: unless-stopped
    environment:
      - POSTGRES_DB=${POSTGRES_DB:-my_practice}
      - POSTGRES_USER=${POSTGRES_USER:-my_practice}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-changeme123}
    # Loopback only: the dev password is a published default, and a bare
    # "5432:5432" listens on every interface (Docker also bypasses ufw).
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-my_practice} -d ${POSTGRES_DB:-my_practice}']
      interval: 10s
      timeout: 5s
      retries: 5

  # One-shot, before the unprivileged services start: hands them the files that
  # earlier root containers left behind, and the volumes an image seeds as root.
  # Only this service keeps a capability to change ownership. It has no network.
  fix-ownership:
    image: my-practice-django
    pull_policy: never
    restart: "no"
    network_mode: none
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_READ_SEARCH
    security_opt:
      - no-new-privileges:true
    volumes:
      - ./app:/app
      - node_modules_django:/app/node_modules
      - static_data:/app/staticfiles
      - ${MY_PRACTICE_DATA_DIR:-../my-practice-data}/media:/app/media
    command:
      - sh
      - -c
      - >-
        find /app ! -user ${MY_PRACTICE_UID:-1000} -exec chown -h ${MY_PRACTICE_UID:-1000} {} + &&
        find /app ! -group ${MY_PRACTICE_GID:-1000} -exec chgrp -h ${MY_PRACTICE_GID:-1000} {} +

  django:
    <<: *unprivileged
    image: my-practice-django
    build:
      context: ./app
      target: development
    container_name: my-practice-django
    restart: unless-stopped
    network_mode: host
    environment:
      # This UID has no passwd entry, so nothing else gives it a writable home
      # (fontconfig and npm write their caches there).
      - HOME=/tmp
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY:-django-insecure-dev-key}
      - DJANGO_DEBUG=${DJANGO_DEBUG:-True}
      - DJANGO_LANGUAGE_CODE=${DJANGO_LANGUAGE_CODE:-de-de}
      - DJANGO_TIME_ZONE=${DJANGO_TIME_ZONE:-Europe/Berlin}
      - DJANGO_SESSION_IDLE_HOURS=${DJANGO_SESSION_IDLE_HOURS:-8}
      - REQUIRE_TWO_FACTOR=${REQUIRE_TWO_FACTOR:-false}
      - POSTGRES_DB=${POSTGRES_DB:-my_practice}
      - POSTGRES_USER=${POSTGRES_USER:-my_practice}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-changeme123}
      - POSTGRES_HOST=127.0.0.1
      - POSTGRES_PORT=5432
      - USE_PROTON_BRIDGE=${USE_PROTON_BRIDGE:-false}
      - EMAIL_HOST=${EMAIL_HOST:-127.0.0.1}
      - EMAIL_PORT=${EMAIL_PORT:-1025}
      - EMAIL_HOST_USER=${EMAIL_HOST_USER:-}
      - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-}
      - DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL:-}
      - GOOGLE_CALENDAR_CLIENT_ID=${GOOGLE_CALENDAR_CLIENT_ID:-}
      - GOOGLE_CALENDAR_CLIENT_SECRET=${GOOGLE_CALENDAR_CLIENT_SECRET:-}
      - FERNET_KEY=${FERNET_KEY:-}
      - MY_PRACTICE_DATA_DIR=/my-practice-data
      - MY_PRACTICE_REPO_ROOT=/repo
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ./app:/app
      # Named volume so the image's `npm install` (app/Dockerfile's development
      # stage) isn't shadowed by the ./app:/app bind mount above — the host
      # checkout has no node_modules/ of its own (see app/.dockerignore).
      # Separate volume per service: both containers populate it from their
      # own image on first start, and sharing one volume races the two.
      - node_modules_django:/app/node_modules
      # test_release_guardrails compares version strings across prod.py and
      # docker-compose.prod.yml at the repo root, which only ./app:/app can't
      # reach. Mounted as a directory, not per file: a single-file bind mount
      # pins the inode, and `sed -i` or an editor save replaces the file, so
      # the container kept seeing the pre-bump version until recreated.
      - .:/repo:ro
      # Backups and media live OUTSIDE the repo at $MY_PRACTICE_DATA_DIR (set in .env).
      # Default: ../my-practice-data (sibling directory next to the repo clone).
      - ${MY_PRACTICE_DATA_DIR:-../my-practice-data}/backups:/app/backups:ro
      - static_data:/app/staticfiles
      - ${MY_PRACTICE_DATA_DIR:-../my-practice-data}/media:/app/media
      - ${MY_PRACTICE_DATA_DIR:-../my-practice-data}/documents:/my-practice-data/documents
      - ${MY_PRACTICE_DATA_DIR:-../my-practice-data}/questionnaires:/my-practice-data/questionnaires
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8000/accounts/login/"]
      interval: 5s
      timeout: 5s
      retries: 24
      start_period: 10s
    depends_on:
      fix-ownership:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
    # gunicorn binds loopback, like postgres above: network_mode: host makes the
    # bind address the only thing between the DEBUG stack (with the real data
    # directories mounted) and the local network.
    # compilemessages: the .mo catalogs are gitignored, and the ./app bind mount
    # hides the copy the image compiled, so the container compiles its own on
    # start. --reload-extra-file then picks up a recompile (e.g. from the
    # post-merge hook) without a restart; gunicorn's --reload only watches .py.
    command: >
      sh -c "python manage.py migrate --noinput &&
             python manage.py collectstatic --noinput --ignore tailwind.css &&
             python manage.py compilemessages -v 0 &&
             gunicorn --bind 127.0.0.1:8000 --workers 1 --timeout 300 --reload --reload-extra-file locale/de/LC_MESSAGES/django.mo --reload-extra-file locale/en/LC_MESSAGES/django.mo config.wsgi:application"

  tailwind:
    <<: *unprivileged
    build:
      context: ./app
      target: development
    container_name: my-practice-tailwind
    restart: unless-stopped
    environment:
      - HOME=/tmp
    depends_on:
      fix-ownership:
        condition: service_completed_successfully
    volumes:
      - ./app:/app
      - node_modules_tailwind:/app/node_modules
    working_dir: /app
    command: npm run watch:css

volumes:
  postgres_data:
    driver: local
  static_data:
    driver: local
  node_modules_django:
    driver: local
  node_modules_tailwind:
    driver: local