From 56fb0c26e9c0957005724daa9f26bcc0d9b07934 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 16:21:41 +0100 Subject: [PATCH 1/8] feat: docker-compose prod setup --- .env.example | 17 ++++++++++- Dockerfile | 8 ++--- README.md | 66 ++++++++++++++++++++++++++++++++++++++--- config/prod.exs | 11 +++++++ config/runtime.exs | 27 +++++++++++++---- docker-compose.prod.yml | 39 ++++++++++++++++++++++++ docker-compose.yml | 9 ++---- 7 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 docker-compose.prod.yml diff --git a/.env.example b/.env.example index ccbd6ed..7559b0a 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,16 @@ -OIDC_CLIENT_SECRET= +# Production Environment Variables for docker-compose.prod.yml +# Copy this file to .env and fill in the actual values + +# Required: Phoenix secrets (generate with: mix phx.gen.secret) +SECRET_KEY_BASE=changeme-run-mix-phx.gen.secret +TOKEN_SIGNING_SECRET=changeme-run-mix-phx.gen.secret + +# Required: Hostname for URL generation +PHX_HOST=localhost + +# Optional: OIDC Configuration +# These have defaults in docker-compose.prod.yml, only override if needed +# OIDC_CLIENT_ID=mv +# OIDC_BASE_URL=http://localhost:8080/auth/v1 +# OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback +# OIDC_CLIENT_SECRET=your-rauthy-client-secret diff --git a/Dockerfile b/Dockerfile index 5503e2e..88468a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ARG BUILDER_IMAGE="hexpm/elixir:1.18.3-erlang-27.3-debian-bullseye-20250317-slim" ARG RUNNER_IMAGE="debian:bullseye-20250317-slim" -FROM ${BUILDER_IMAGE} as builder +FROM ${BUILDER_IMAGE} AS builder # install build dependencies RUN apt-get update -y && apt-get install -y build-essential git \ @@ -70,9 +70,9 @@ RUN apt-get update -y && \ # Set the locale RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 WORKDIR "/app" RUN chown nobody /app diff --git a/README.md b/README.md index 60bb115..f9fb96f 100644 --- a/README.md +++ b/README.md @@ -183,12 +183,70 @@ Useful `just` commands: - `just reset-database` — reset local DB - `just regen-migrations ` — regenerate migrations -## 📦 Deployment +## 📦 Production Deployment -A production release image is built via the provided `Dockerfile`. -Entrypoint: `/app/bin/server`. +### Local Production Testing + +For testing the production Docker build locally: + +1. **Generate secrets:** + ```bash + mix phx.gen.secret # for SECRET_KEY_BASE + mix phx.gen.secret # for TOKEN_SIGNING_SECRET + ``` + +2. **Create `.env` file:** + ```bash + # Copy template and edit + cp .env.example .env + nano .env + + # Required variables: + SECRET_KEY_BASE= + TOKEN_SIGNING_SECRET= + PHX_HOST=localhost + + # Optional (have defaults in docker-compose.prod.yml): + # OIDC_CLIENT_ID=mv + # OIDC_BASE_URL=http://localhost:8080/auth/v1 + # OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback + # OIDC_CLIENT_SECRET= + ``` + +3. **Start development environment** (for Rauthy): + ```bash + docker compose up -d + ``` + +4. **Build and start production environment:** + ```bash + docker compose -f docker-compose.prod.yml up --build + ``` + +5. **Run database migrations:** + ```bash + docker compose -f docker-compose.prod.yml exec app /app/bin/mv eval "Mv.Release.migrate" + ``` + +6. **Access the production app:** + - Production App: http://localhost:4001 + - Uses same Rauthy instance as dev (localhost:8080) + +**Note:** The local production setup uses `network_mode: host` to share localhost with the development Rauthy instance. For real production deployment, configure an external OIDC provider and remove `network_mode: host`. + +### Real Production Deployment + +For actual production deployment: + +1. **Use an external OIDC provider** (not the local Rauthy) +2. **Update `docker-compose.prod.yml`:** + - Remove `network_mode: host` + - Set `OIDC_BASE_URL` to your production OIDC provider + - Configure proper Docker networks +3. **Set up SSL/TLS** (e.g., via reverse proxy like Nginx/Traefik) +4. **Use secure secrets management** (environment variables, Docker secrets, vault) +5. **Configure database backups** -More detailed deployment docs are planned. ## 🤝 Contributing diff --git a/config/prod.exs b/config/prod.exs index 70ea4b0..e0d0670 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -16,5 +16,16 @@ config :swoosh, local: false # Do not print debug messages in production config :logger, level: :info +# AshAuthentication production configuration +# These must be set at compile-time (not in runtime.exs) because +# Application.compile_env!/3 is used in lib/accounts/user.ex +config :mv, :session_identifier, :jti + +config :mv, :require_token_presence_for_authentication, true + +# Token signing secret - using a placeholder that MUST be overridden +# at runtime via environment variable in config/runtime.exs +config :mv, :token_signing_secret, "REPLACE_ME_AT_RUNTIME" + # Runtime production configuration, including reading # of environment variables, is done on config/runtime.exs. diff --git a/config/runtime.exs b/config/runtime.exs index 464d8cd..fbf5af3 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -53,12 +53,23 @@ if config_env() == :prod do config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") - config :mv, :rauthy, redirect_uri: "http://localhost:4000/auth/user/rauthy/callback" + # Rauthy OIDC configuration + config :mv, :rauthy, + client_id: System.get_env("OIDC_CLIENT_ID") || "mv", + base_url: System.get_env("OIDC_BASE_URL") || "http://localhost:8080/auth/v1", + client_secret: System.get_env("OIDC_CLIENT_SECRET"), + redirect_uri: System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback" - # AshAuthentication production configuration - config :mv, :session_identifier, :jti + # Token signing secret from environment variable + # This overrides the placeholder value set in prod.exs + token_signing_secret = + System.get_env("TOKEN_SIGNING_SECRET") || + raise """ + environment variable TOKEN_SIGNING_SECRET is missing. + You can generate one by calling: mix phx.gen.secret + """ - config :mv, :require_token_presence_for_authentication, true + config :mv, :token_signing_secret, token_signing_secret config :mv, MvWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], @@ -70,7 +81,13 @@ if config_env() == :prod do ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: port ], - secret_key_base: secret_key_base + secret_key_base: secret_key_base, + # Allow connections from localhost and 127.0.0.1 + check_origin: [ + "//#{host}", + "//localhost:#{port}", + "//127.0.0.1:#{port}" + ] # ## SSL Support # diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..2030049 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,39 @@ +services: + app: + build: . + image: mv:latest + container_name: mv-prod-app + # Use host network for local testing to access localhost:8080 (Rauthy) + # In real production, remove this and use external OIDC provider + network_mode: host + environment: + DATABASE_URL: "ecto://postgres:postgres@localhost:5001/mv_prod" + SECRET_KEY_BASE: "${SECRET_KEY_BASE}" + TOKEN_SIGNING_SECRET: "${TOKEN_SIGNING_SECRET}" + PHX_HOST: "${PHX_HOST}" + PORT: "4001" + PHX_SERVER: "true" + # Rauthy OIDC config - uses localhost because of host network mode + OIDC_CLIENT_ID: "mv" + OIDC_BASE_URL: "http://localhost:8080/auth/v1" + OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET:-}" + OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback" + depends_on: + - db-prod + restart: unless-stopped + + db-prod: + image: postgres:16-alpine + container_name: mv-prod-db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mv_prod + volumes: + - postgres_data_prod:/var/lib/postgresql/data + ports: + - "5001:5432" + restart: unless-stopped + +volumes: + postgres_data_prod: diff --git a/docker-compose.yml b/docker-compose.yml index 297b4af..56876f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ networks: local: rauthy-dev: - driver: bridge services: db: @@ -39,12 +38,8 @@ services: - LISTEN_SCHEME=http - PUB_URL=localhost:8080 - BOOTSTRAP_ADMIN_PASSWORD_PLAIN=RauthyTest12345 - #- HIQLITE=false - #- PG_HOST=db - #- PG_PORT=5432 - #- PG_USER=postgres - #- PG_PASSWORD=postgres - #- PG_DB_NAME=mv_dev + # Disable strict IP validation to allow access from multiple Docker networks + - SESSION_VALIDATE_IP=false ports: - "8080:8080" depends_on: From c076b47c77640a4e277bfc1b60f438dcd46f84d9 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 18:46:29 +0100 Subject: [PATCH 2/8] test: add ci-build-container to pipeline trigger for testing --- .drone.yml | 66 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/.drone.yml b/.drone.yml index 860e82c..2c77dc2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,43 +1,61 @@ kind: pipeline type: docker -name: check +name: build-and-publish services: - - name: postgres - image: docker.io/library/postgres:17.6 - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - name: docker image: docker:dind privileged: true - volumes: - - name: dockersock - path: /var/run + environment: + DOCKER_TLS_CERTDIR: "" # Disable TLS and Unix socket - use TCP only trigger: + branch: + - main + - ci-build-container # TODO: Remove after testing event: - push + - tag steps: - - name: build & publish container? - image: docker:dind - volumes: - - name: dockersock - path: /var/run + - name: build-and-publish-container + image: docker:cli + environment: + DOCKER_HOST: tcp://docker:2375 # Connect to dind service + REGISTRY: git.local-it.org/ci-builder + IMAGE_NAME: mitgliederverwaltung + REGISTRY_USERNAME: + from_secret: DRONE_REGISTRY_USERNAME + REGISTRY_TOKEN: + from_secret: DRONE_REGISTRY_TOKEN commands: - - sleep 6 # give docker time to start - - docker build --tag git.local-it.org/ci-builder/mitgliederverwaltung:latest . - - docker login --username $DRONE_FORGEJO_ACCOUNT_USERNAME --password $DRONE_FORGEJO_ACCOUNT_PASSWORD git.local-it.org - - docker push git.local-it.org/ci-builder/mitgliederverwaltung:latest + - sleep 5 # give docker time to start + - docker info # verify docker is ready + # Build image once + - docker build --tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER . + # Login to registry + - echo "$REGISTRY_TOKEN" | docker login --username "$REGISTRY_USERNAME" --password-stdin git.local-it.org + # Tag and push based on event type + - | + if [ "$DRONE_BUILD_EVENT" = "tag" ]; then + # For tag events: use tag version (e.g., v1.0.0 -> 1.0.0) and latest + VERSION=$(echo $DRONE_TAG | sed 's/^v//') + echo "Tagging and pushing version $VERSION" + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$VERSION + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest + docker push $REGISTRY/$IMAGE_NAME:$VERSION + docker push $REGISTRY/$IMAGE_NAME:latest + else + # For main branch pushes: use commit SHA and latest + echo "Tagging and pushing commit $DRONE_COMMIT_SHA" + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest + docker push $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA + docker push $REGISTRY/$IMAGE_NAME:latest + fi -volumes: - - name: cache - host: - path: /tmp/drone_cache - - name: dockersock - temp: {} +# No volumes needed - docker:cli connects to dind service via TCP --- kind: pipeline From a5b8f52d77846fb4c310133809ec4bb9dc81fe09 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 19:17:37 +0100 Subject: [PATCH 3/8] refactor: use plugins/docker instead of manual dind setup --- .drone.yml | 69 +++++++++++++++++++++--------------------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/.drone.yml b/.drone.yml index 2c77dc2..a243ee9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,13 +2,6 @@ kind: pipeline type: docker name: build-and-publish -services: - - name: docker - image: docker:dind - privileged: true - environment: - DOCKER_TLS_CERTDIR: "" # Disable TLS and Unix socket - use TCP only - trigger: branch: - main @@ -19,43 +12,35 @@ trigger: steps: - name: build-and-publish-container - image: docker:cli - environment: - DOCKER_HOST: tcp://docker:2375 # Connect to dind service - REGISTRY: git.local-it.org/ci-builder - IMAGE_NAME: mitgliederverwaltung - REGISTRY_USERNAME: + image: plugins/docker + settings: + registry: git.local-it.org + repo: git.local-it.org/local-it/mitgliederverwaltung + username: from_secret: DRONE_REGISTRY_USERNAME - REGISTRY_TOKEN: + password: from_secret: DRONE_REGISTRY_TOKEN - commands: - - sleep 5 # give docker time to start - - docker info # verify docker is ready - # Build image once - - docker build --tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER . - # Login to registry - - echo "$REGISTRY_TOKEN" | docker login --username "$REGISTRY_USERNAME" --password-stdin git.local-it.org - # Tag and push based on event type - - | - if [ "$DRONE_BUILD_EVENT" = "tag" ]; then - # For tag events: use tag version (e.g., v1.0.0 -> 1.0.0) and latest - VERSION=$(echo $DRONE_TAG | sed 's/^v//') - echo "Tagging and pushing version $VERSION" - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$VERSION - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest - docker push $REGISTRY/$IMAGE_NAME:$VERSION - docker push $REGISTRY/$IMAGE_NAME:latest - else - # For main branch pushes: use commit SHA and latest - echo "Tagging and pushing commit $DRONE_COMMIT_SHA" - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest - docker push $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA - docker push $REGISTRY/$IMAGE_NAME:latest - fi - - -# No volumes needed - docker:cli connects to dind service via TCP + auto_tag: true + auto_tag_suffix: ${DRONE_COMMIT_SHA:0:8} + when: + event: + - tag + + - name: build-and-publish-container-branch + image: plugins/docker + settings: + registry: git.local-it.org + repo: git.local-it.org/local-it/mitgliederverwaltung + username: + from_secret: DRONE_REGISTRY_USERNAME + password: + from_secret: DRONE_REGISTRY_TOKEN + tags: + - latest + - ${DRONE_COMMIT_SHA:0:8} + when: + event: + - push --- kind: pipeline From 60f2e972dd7fafff15d4043be5c8cc659e74757b Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 19:57:40 +0100 Subject: [PATCH 4/8] Revert "dropme: remove other drone tasks for faster debugging" This reverts commit cdc91aec574636a7811f4f2bb9b7d3329569c7a8. --- .drone.yml | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index a243ee9..1463969 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,5 +1,95 @@ kind: pipeline type: docker +name: check + +services: + - name: postgres + image: docker.io/library/postgres:17.6 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + +trigger: + event: + - push + +steps: + - name: compute cache key + image: docker.io/library/elixir:1.18.3-otp-27 + commands: + - mix_lock_hash=$(sha256sum mix.lock | cut -d ' ' -f 1) + - echo "$DRONE_REPO_OWNER/$DRONE_REPO_NAME/$mix_lock_hash" >> .cache_key + - cat .cache_key + + - name: restore-cache + image: drillster/drone-volume-cache + settings: + restore: true + mount: + - ./deps + - ./_build + ttl: 30 + volumes: + - name: cache + path: /cache + + - name: lint + image: docker.io/library/elixir:1.18.3-otp-27 + commands: + - mix local.hex --force + - mix deps.get + - mix compile --warnings-as-errors + - mix format --check-formatted + - mix sobelow --config + - mix deps.audit + - mix hex.audit + - mix credo + + - name: wait_for_postgres + image: docker.io/library/postgres:17.6 + commands: + - | + for i in {1..20}; do + if pg_isready -h postgres -U postgres; then + exit 0 + else + true + fi + sleep 2 + done + echo "Postgres did not become available, aborting." + exit 1 + + - name: test + image: docker.io/library/elixir:1.18.3-otp-27 + environment: + MIX_ENV: test + TEST_POSTGRES_HOST: postgres + TEST_POSTGRES_PORT: 5432 + commands: + - mix local.hex --force + - mix deps.get + - mix test + + - name: rebuild-cache + image: drillster/drone-volume-cache + settings: + rebuild: true + mount: + - ./deps + - ./_build + volumes: + - name: cache + path: /cache + +volumes: + - name: cache + host: + path: /tmp/drone_cache + +--- +kind: pipeline +type: docker name: build-and-publish trigger: @@ -25,7 +115,7 @@ steps: when: event: - tag - + - name: build-and-publish-container-branch image: plugins/docker settings: @@ -42,6 +132,9 @@ steps: event: - push +depends_on: + - check + --- kind: pipeline type: docker From d3fd4d6c0ecd8e1f21947fbc36704189ee34d440 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 16:21:41 +0100 Subject: [PATCH 5/8] feat: docker-compose prod setup --- .env.example | 17 ++++++++++- Dockerfile | 8 ++--- README.md | 66 ++++++++++++++++++++++++++++++++++++++--- config/prod.exs | 11 +++++++ config/runtime.exs | 28 +++++++++++++---- docker-compose.prod.yml | 39 ++++++++++++++++++++++++ docker-compose.yml | 9 ++---- 7 files changed, 157 insertions(+), 21 deletions(-) create mode 100644 docker-compose.prod.yml diff --git a/.env.example b/.env.example index ccbd6ed..7559b0a 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,16 @@ -OIDC_CLIENT_SECRET= +# Production Environment Variables for docker-compose.prod.yml +# Copy this file to .env and fill in the actual values + +# Required: Phoenix secrets (generate with: mix phx.gen.secret) +SECRET_KEY_BASE=changeme-run-mix-phx.gen.secret +TOKEN_SIGNING_SECRET=changeme-run-mix-phx.gen.secret + +# Required: Hostname for URL generation +PHX_HOST=localhost + +# Optional: OIDC Configuration +# These have defaults in docker-compose.prod.yml, only override if needed +# OIDC_CLIENT_ID=mv +# OIDC_BASE_URL=http://localhost:8080/auth/v1 +# OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback +# OIDC_CLIENT_SECRET=your-rauthy-client-secret diff --git a/Dockerfile b/Dockerfile index 5503e2e..88468a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ARG BUILDER_IMAGE="hexpm/elixir:1.18.3-erlang-27.3-debian-bullseye-20250317-slim" ARG RUNNER_IMAGE="debian:bullseye-20250317-slim" -FROM ${BUILDER_IMAGE} as builder +FROM ${BUILDER_IMAGE} AS builder # install build dependencies RUN apt-get update -y && apt-get install -y build-essential git \ @@ -70,9 +70,9 @@ RUN apt-get update -y && \ # Set the locale RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 WORKDIR "/app" RUN chown nobody /app diff --git a/README.md b/README.md index 60bb115..f9fb96f 100644 --- a/README.md +++ b/README.md @@ -183,12 +183,70 @@ Useful `just` commands: - `just reset-database` — reset local DB - `just regen-migrations ` — regenerate migrations -## 📦 Deployment +## 📦 Production Deployment -A production release image is built via the provided `Dockerfile`. -Entrypoint: `/app/bin/server`. +### Local Production Testing + +For testing the production Docker build locally: + +1. **Generate secrets:** + ```bash + mix phx.gen.secret # for SECRET_KEY_BASE + mix phx.gen.secret # for TOKEN_SIGNING_SECRET + ``` + +2. **Create `.env` file:** + ```bash + # Copy template and edit + cp .env.example .env + nano .env + + # Required variables: + SECRET_KEY_BASE= + TOKEN_SIGNING_SECRET= + PHX_HOST=localhost + + # Optional (have defaults in docker-compose.prod.yml): + # OIDC_CLIENT_ID=mv + # OIDC_BASE_URL=http://localhost:8080/auth/v1 + # OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback + # OIDC_CLIENT_SECRET= + ``` + +3. **Start development environment** (for Rauthy): + ```bash + docker compose up -d + ``` + +4. **Build and start production environment:** + ```bash + docker compose -f docker-compose.prod.yml up --build + ``` + +5. **Run database migrations:** + ```bash + docker compose -f docker-compose.prod.yml exec app /app/bin/mv eval "Mv.Release.migrate" + ``` + +6. **Access the production app:** + - Production App: http://localhost:4001 + - Uses same Rauthy instance as dev (localhost:8080) + +**Note:** The local production setup uses `network_mode: host` to share localhost with the development Rauthy instance. For real production deployment, configure an external OIDC provider and remove `network_mode: host`. + +### Real Production Deployment + +For actual production deployment: + +1. **Use an external OIDC provider** (not the local Rauthy) +2. **Update `docker-compose.prod.yml`:** + - Remove `network_mode: host` + - Set `OIDC_BASE_URL` to your production OIDC provider + - Configure proper Docker networks +3. **Set up SSL/TLS** (e.g., via reverse proxy like Nginx/Traefik) +4. **Use secure secrets management** (environment variables, Docker secrets, vault) +5. **Configure database backups** -More detailed deployment docs are planned. ## 🤝 Contributing diff --git a/config/prod.exs b/config/prod.exs index 70ea4b0..e0d0670 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -16,5 +16,16 @@ config :swoosh, local: false # Do not print debug messages in production config :logger, level: :info +# AshAuthentication production configuration +# These must be set at compile-time (not in runtime.exs) because +# Application.compile_env!/3 is used in lib/accounts/user.ex +config :mv, :session_identifier, :jti + +config :mv, :require_token_presence_for_authentication, true + +# Token signing secret - using a placeholder that MUST be overridden +# at runtime via environment variable in config/runtime.exs +config :mv, :token_signing_secret, "REPLACE_ME_AT_RUNTIME" + # Runtime production configuration, including reading # of environment variables, is done on config/runtime.exs. diff --git a/config/runtime.exs b/config/runtime.exs index 464d8cd..c50356c 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -53,12 +53,24 @@ if config_env() == :prod do config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") - config :mv, :rauthy, redirect_uri: "http://localhost:4000/auth/user/rauthy/callback" + # Rauthy OIDC configuration + config :mv, :rauthy, + client_id: System.get_env("OIDC_CLIENT_ID") || "mv", + base_url: System.get_env("OIDC_BASE_URL") || "http://localhost:8080/auth/v1", + client_secret: System.get_env("OIDC_CLIENT_SECRET"), + redirect_uri: + System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback" - # AshAuthentication production configuration - config :mv, :session_identifier, :jti + # Token signing secret from environment variable + # This overrides the placeholder value set in prod.exs + token_signing_secret = + System.get_env("TOKEN_SIGNING_SECRET") || + raise """ + environment variable TOKEN_SIGNING_SECRET is missing. + You can generate one by calling: mix phx.gen.secret + """ - config :mv, :require_token_presence_for_authentication, true + config :mv, :token_signing_secret, token_signing_secret config :mv, MvWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], @@ -70,7 +82,13 @@ if config_env() == :prod do ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: port ], - secret_key_base: secret_key_base + secret_key_base: secret_key_base, + # Allow connections from localhost and 127.0.0.1 + check_origin: [ + "//#{host}", + "//localhost:#{port}", + "//127.0.0.1:#{port}" + ] # ## SSL Support # diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..2030049 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,39 @@ +services: + app: + build: . + image: mv:latest + container_name: mv-prod-app + # Use host network for local testing to access localhost:8080 (Rauthy) + # In real production, remove this and use external OIDC provider + network_mode: host + environment: + DATABASE_URL: "ecto://postgres:postgres@localhost:5001/mv_prod" + SECRET_KEY_BASE: "${SECRET_KEY_BASE}" + TOKEN_SIGNING_SECRET: "${TOKEN_SIGNING_SECRET}" + PHX_HOST: "${PHX_HOST}" + PORT: "4001" + PHX_SERVER: "true" + # Rauthy OIDC config - uses localhost because of host network mode + OIDC_CLIENT_ID: "mv" + OIDC_BASE_URL: "http://localhost:8080/auth/v1" + OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET:-}" + OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback" + depends_on: + - db-prod + restart: unless-stopped + + db-prod: + image: postgres:16-alpine + container_name: mv-prod-db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mv_prod + volumes: + - postgres_data_prod:/var/lib/postgresql/data + ports: + - "5001:5432" + restart: unless-stopped + +volumes: + postgres_data_prod: diff --git a/docker-compose.yml b/docker-compose.yml index 297b4af..56876f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ networks: local: rauthy-dev: - driver: bridge services: db: @@ -39,12 +38,8 @@ services: - LISTEN_SCHEME=http - PUB_URL=localhost:8080 - BOOTSTRAP_ADMIN_PASSWORD_PLAIN=RauthyTest12345 - #- HIQLITE=false - #- PG_HOST=db - #- PG_PORT=5432 - #- PG_USER=postgres - #- PG_PASSWORD=postgres - #- PG_DB_NAME=mv_dev + # Disable strict IP validation to allow access from multiple Docker networks + - SESSION_VALIDATE_IP=false ports: - "8080:8080" depends_on: From 2a4dbc981cde35d39a5976e0243330ceebd3eecd Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 18:46:29 +0100 Subject: [PATCH 6/8] test: add ci-build-container to pipeline trigger for testing --- .drone.yml | 66 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/.drone.yml b/.drone.yml index 860e82c..2c77dc2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,43 +1,61 @@ kind: pipeline type: docker -name: check +name: build-and-publish services: - - name: postgres - image: docker.io/library/postgres:17.6 - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - name: docker image: docker:dind privileged: true - volumes: - - name: dockersock - path: /var/run + environment: + DOCKER_TLS_CERTDIR: "" # Disable TLS and Unix socket - use TCP only trigger: + branch: + - main + - ci-build-container # TODO: Remove after testing event: - push + - tag steps: - - name: build & publish container? - image: docker:dind - volumes: - - name: dockersock - path: /var/run + - name: build-and-publish-container + image: docker:cli + environment: + DOCKER_HOST: tcp://docker:2375 # Connect to dind service + REGISTRY: git.local-it.org/ci-builder + IMAGE_NAME: mitgliederverwaltung + REGISTRY_USERNAME: + from_secret: DRONE_REGISTRY_USERNAME + REGISTRY_TOKEN: + from_secret: DRONE_REGISTRY_TOKEN commands: - - sleep 6 # give docker time to start - - docker build --tag git.local-it.org/ci-builder/mitgliederverwaltung:latest . - - docker login --username $DRONE_FORGEJO_ACCOUNT_USERNAME --password $DRONE_FORGEJO_ACCOUNT_PASSWORD git.local-it.org - - docker push git.local-it.org/ci-builder/mitgliederverwaltung:latest + - sleep 5 # give docker time to start + - docker info # verify docker is ready + # Build image once + - docker build --tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER . + # Login to registry + - echo "$REGISTRY_TOKEN" | docker login --username "$REGISTRY_USERNAME" --password-stdin git.local-it.org + # Tag and push based on event type + - | + if [ "$DRONE_BUILD_EVENT" = "tag" ]; then + # For tag events: use tag version (e.g., v1.0.0 -> 1.0.0) and latest + VERSION=$(echo $DRONE_TAG | sed 's/^v//') + echo "Tagging and pushing version $VERSION" + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$VERSION + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest + docker push $REGISTRY/$IMAGE_NAME:$VERSION + docker push $REGISTRY/$IMAGE_NAME:latest + else + # For main branch pushes: use commit SHA and latest + echo "Tagging and pushing commit $DRONE_COMMIT_SHA" + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA + docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest + docker push $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA + docker push $REGISTRY/$IMAGE_NAME:latest + fi -volumes: - - name: cache - host: - path: /tmp/drone_cache - - name: dockersock - temp: {} +# No volumes needed - docker:cli connects to dind service via TCP --- kind: pipeline From 4bfaeb1b6e0bf8f9f11078cdf1e50ec02ccdfe4a Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 19:17:37 +0100 Subject: [PATCH 7/8] refactor: use plugins/docker instead of manual dind setup --- .drone.yml | 69 +++++++++++++++++++++--------------------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/.drone.yml b/.drone.yml index 2c77dc2..a243ee9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,13 +2,6 @@ kind: pipeline type: docker name: build-and-publish -services: - - name: docker - image: docker:dind - privileged: true - environment: - DOCKER_TLS_CERTDIR: "" # Disable TLS and Unix socket - use TCP only - trigger: branch: - main @@ -19,43 +12,35 @@ trigger: steps: - name: build-and-publish-container - image: docker:cli - environment: - DOCKER_HOST: tcp://docker:2375 # Connect to dind service - REGISTRY: git.local-it.org/ci-builder - IMAGE_NAME: mitgliederverwaltung - REGISTRY_USERNAME: + image: plugins/docker + settings: + registry: git.local-it.org + repo: git.local-it.org/local-it/mitgliederverwaltung + username: from_secret: DRONE_REGISTRY_USERNAME - REGISTRY_TOKEN: + password: from_secret: DRONE_REGISTRY_TOKEN - commands: - - sleep 5 # give docker time to start - - docker info # verify docker is ready - # Build image once - - docker build --tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER . - # Login to registry - - echo "$REGISTRY_TOKEN" | docker login --username "$REGISTRY_USERNAME" --password-stdin git.local-it.org - # Tag and push based on event type - - | - if [ "$DRONE_BUILD_EVENT" = "tag" ]; then - # For tag events: use tag version (e.g., v1.0.0 -> 1.0.0) and latest - VERSION=$(echo $DRONE_TAG | sed 's/^v//') - echo "Tagging and pushing version $VERSION" - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$VERSION - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest - docker push $REGISTRY/$IMAGE_NAME:$VERSION - docker push $REGISTRY/$IMAGE_NAME:latest - else - # For main branch pushes: use commit SHA and latest - echo "Tagging and pushing commit $DRONE_COMMIT_SHA" - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA - docker tag $REGISTRY/$IMAGE_NAME:build-$DRONE_BUILD_NUMBER $REGISTRY/$IMAGE_NAME:latest - docker push $REGISTRY/$IMAGE_NAME:$DRONE_COMMIT_SHA - docker push $REGISTRY/$IMAGE_NAME:latest - fi - - -# No volumes needed - docker:cli connects to dind service via TCP + auto_tag: true + auto_tag_suffix: ${DRONE_COMMIT_SHA:0:8} + when: + event: + - tag + + - name: build-and-publish-container-branch + image: plugins/docker + settings: + registry: git.local-it.org + repo: git.local-it.org/local-it/mitgliederverwaltung + username: + from_secret: DRONE_REGISTRY_USERNAME + password: + from_secret: DRONE_REGISTRY_TOKEN + tags: + - latest + - ${DRONE_COMMIT_SHA:0:8} + when: + event: + - push --- kind: pipeline From 581209510ceb6c4f113e805039858188d7a8fada Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 30 Oct 2025 19:57:40 +0100 Subject: [PATCH 8/8] Revert "dropme: remove other drone tasks for faster debugging" This reverts commit cdc91aec574636a7811f4f2bb9b7d3329569c7a8. --- .drone.yml | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index a243ee9..1463969 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,5 +1,95 @@ kind: pipeline type: docker +name: check + +services: + - name: postgres + image: docker.io/library/postgres:17.6 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + +trigger: + event: + - push + +steps: + - name: compute cache key + image: docker.io/library/elixir:1.18.3-otp-27 + commands: + - mix_lock_hash=$(sha256sum mix.lock | cut -d ' ' -f 1) + - echo "$DRONE_REPO_OWNER/$DRONE_REPO_NAME/$mix_lock_hash" >> .cache_key + - cat .cache_key + + - name: restore-cache + image: drillster/drone-volume-cache + settings: + restore: true + mount: + - ./deps + - ./_build + ttl: 30 + volumes: + - name: cache + path: /cache + + - name: lint + image: docker.io/library/elixir:1.18.3-otp-27 + commands: + - mix local.hex --force + - mix deps.get + - mix compile --warnings-as-errors + - mix format --check-formatted + - mix sobelow --config + - mix deps.audit + - mix hex.audit + - mix credo + + - name: wait_for_postgres + image: docker.io/library/postgres:17.6 + commands: + - | + for i in {1..20}; do + if pg_isready -h postgres -U postgres; then + exit 0 + else + true + fi + sleep 2 + done + echo "Postgres did not become available, aborting." + exit 1 + + - name: test + image: docker.io/library/elixir:1.18.3-otp-27 + environment: + MIX_ENV: test + TEST_POSTGRES_HOST: postgres + TEST_POSTGRES_PORT: 5432 + commands: + - mix local.hex --force + - mix deps.get + - mix test + + - name: rebuild-cache + image: drillster/drone-volume-cache + settings: + rebuild: true + mount: + - ./deps + - ./_build + volumes: + - name: cache + path: /cache + +volumes: + - name: cache + host: + path: /tmp/drone_cache + +--- +kind: pipeline +type: docker name: build-and-publish trigger: @@ -25,7 +115,7 @@ steps: when: event: - tag - + - name: build-and-publish-container-branch image: plugins/docker settings: @@ -42,6 +132,9 @@ steps: event: - push +depends_on: + - check + --- kind: pipeline type: docker