├── certs └── .keep ├── cron └── .keep ├── getmail └── .keep ├── gpg └── .keep ├── msmtp └── .keep ├── smime └── .keep ├── shredder └── .keep ├── .prettierignore ├── .kube-linter.yml ├── logs_prod.sh ├── k8s-jobs ├── install-ingress.sh ├── db-init.yaml └── db-update.yaml ├── .yamllint.yml ├── helm ├── Chart.yaml ├── templates │ ├── serviceaccount.yaml │ ├── configmap.yaml │ ├── pvc-postgres.yaml │ ├── route.yaml │ ├── service-rt.yaml │ ├── service-postgres.yaml │ ├── _helpers.tpl │ ├── hpa-rt.yaml │ ├── hpa-postgres.yaml │ ├── ingress.yaml │ ├── NOTES.txt │ ├── pvc.yaml │ ├── jobs.yaml │ ├── deployment-postgres.yaml │ └── deployment-rt.yaml ├── .helmignore └── values.yaml ├── msmtp.conf.example ├── cron_entrypoint.sh ├── .dockerignore ├── getmailrc.example ├── restart_prod.sh ├── prod.sh ├── .github ├── workflows │ ├── kubelint.yml │ ├── yamllint.yml │ ├── hadolint.yml │ ├── auto-merge-dependabot.yml │ └── docker.yml └── dependabot.yml ├── .gitignore ├── Caddyfile.example ├── dev.sh ├── dev-helm.sh ├── crontab.example ├── .goreleaser.yaml ├── RT_SiteConfig.pm.example ├── docker-compose.dev.yml ├── bash_functions.sh ├── docker-compose.yml ├── Readme.md └── Dockerfile /certs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cron/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /getmail/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gpg/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /msmtp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /smime/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shredder/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md 2 | helm -------------------------------------------------------------------------------- /.kube-linter.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | exclude: 3 | - latest-tag 4 | -------------------------------------------------------------------------------- /logs_prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker compose logs -f --tail=100 4 | -------------------------------------------------------------------------------- /k8s-jobs/install-ingress.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | helm install --namespace kube-system nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginx 4 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | ignore: 5 | - helm/ 6 | 7 | rules: 8 | truthy: disable 9 | line-length: disable 10 | document-start: disable 11 | comments: 12 | min-spaces-from-content: 1 13 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: request-tracker 3 | description: A Helm chart for installing request tracker on Kubernetes/OpenShift clusters. 4 | type: application 5 | version: 0.1.0 6 | sources: 7 | - https://github.com/firefart/rt-docker 8 | maintainers: 9 | - name: "Christian Mehlmauer" 10 | -------------------------------------------------------------------------------- /msmtp.conf.example: -------------------------------------------------------------------------------- 1 | defaults 2 | 3 | account smtp 4 | host smtp.office365.com 5 | port 587 6 | tls on 7 | tls_starttls on 8 | from test@test.com 9 | tls_trust_file /msmtp/ca-bundle.crt 10 | tls_certcheck on 11 | auth on 12 | user user@domain.com 13 | password pass 14 | logfile - 15 | 16 | account default : smtp 17 | -------------------------------------------------------------------------------- /cron_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # workaournd as docker configs are mounted and do not support the uid parameter: 4 | # https://github.com/docker/compose/issues/9648 5 | 6 | # cron.d files need to have special permissions 7 | cp /crontab /etc/cron.d/crontab 8 | chown root:root /etc/cron.d/crontab 9 | chmod 0644 /etc/cron.d/crontab 10 | 11 | /usr/sbin/cron -f 12 | -------------------------------------------------------------------------------- /helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: rt 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /helm/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: rt-config 5 | labels: 6 | {{- include "request-tracker.labels" . | nindent 4 }} 7 | data: 8 | rtSiteConfig: |- 9 | {{ .Values.config.rtSiteConfig | indent 4}} 10 | msmtp: |- 11 | {{ .Values.config.msmtp | indent 4 }} 12 | getmailrc: |- 13 | {{ .Values.config.getmailrc | indent 4 }} 14 | caddyfile: |- 15 | {{ .Values.config.caddyfile | indent 4 }} 16 | -------------------------------------------------------------------------------- /helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | *.md 3 | *.pem 4 | *.key 5 | *.yml 6 | *.example 7 | *.secret 8 | *.env 9 | .github/ 10 | .git/ 11 | certs/ 12 | cron/ 13 | getmail/ 14 | gpg/ 15 | msmtp/ 16 | shredder/ 17 | smime/ 18 | .env 19 | .keep 20 | **/.keep 21 | *.pgpass 22 | *.json 23 | !cron_entrypoint.sh 24 | crontab 25 | RT_SiteConfig.pm 26 | Caddyfile 27 | Dockerfile 28 | docker-compose.yml 29 | docker-compose.override.yml 30 | docker-compose.dev.yml 31 | .dockerignore 32 | .gitignore 33 | *.patch 34 | TODO -------------------------------------------------------------------------------- /getmailrc.example: -------------------------------------------------------------------------------- 1 | [retriever] 2 | type = SimpleIMAPSSLRetriever 3 | server = mail.host.com 4 | username = user@domain.com 5 | password = pass 6 | mailboxes = ("INBOX",) 7 | 8 | [destination] 9 | type = MDA_external 10 | path = /opt/rt/bin/rt-mailgate 11 | user = rt 12 | group = rt 13 | # 8080 is the mailgate vhost 14 | arguments = ("--url", "http://caddy:8080/", "--queue", "general", "--action", "correspond",) 15 | 16 | [options] 17 | read_all = false 18 | delete = true 19 | verbose = 0 20 | -------------------------------------------------------------------------------- /restart_prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this only restarts prod without pulling in new images 4 | 5 | set -euf -o pipefail 6 | 7 | export DOCKER_BUILDKIT=1 8 | export COMPOSE_DOCKER_CLI_BUILD=1 9 | 10 | DIR="${BASH_SOURCE%/*}" 11 | if [[ ! -d "${DIR}" ]]; then DIR="${PWD}"; fi 12 | . "${DIR}/bash_functions.sh" 13 | 14 | check_files 15 | 16 | fix_file_perms 17 | 18 | docker compose stop 19 | docker compose rm -f -v -s 20 | docker compose up -d --remove-orphans 21 | docker image prune -f 22 | -------------------------------------------------------------------------------- /prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this pulls in new images and restarts everything 4 | 5 | set -euf -o pipefail 6 | 7 | export DOCKER_BUILDKIT=1 8 | export COMPOSE_DOCKER_CLI_BUILD=1 9 | 10 | DIR="${BASH_SOURCE%/*}" 11 | if [[ ! -d "${DIR}" ]]; then DIR="${PWD}"; fi 12 | . "${DIR}/bash_functions.sh" 13 | 14 | check_files 15 | 16 | fix_file_perms 17 | 18 | docker compose pull 19 | docker compose stop 20 | docker compose rm -f -v -s 21 | docker compose up -d --remove-orphans 22 | docker image prune -f 23 | -------------------------------------------------------------------------------- /.github/workflows/kubelint.yml: -------------------------------------------------------------------------------- 1 | name: kubelint 2 | on: 3 | push: 4 | paths: 5 | - "helm/**.yml" 6 | - "helm/**.yaml" 7 | pull_request: 8 | workflow_dispatch: 9 | permissions: 10 | contents: read 11 | jobs: 12 | kubelint: 13 | name: kubelint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: checkout 17 | uses: actions/checkout@v6 18 | 19 | - name: Scan repo with kube-linter 20 | uses: stackrox/kube-linter-action@v1.0.7 21 | with: 22 | directory: helm 23 | -------------------------------------------------------------------------------- /.github/workflows/yamllint.yml: -------------------------------------------------------------------------------- 1 | name: yamllint 2 | on: 3 | push: 4 | paths: 5 | - "**.yml" 6 | - "**.yaml" 7 | pull_request: 8 | workflow_dispatch: 9 | permissions: 10 | contents: read 11 | jobs: 12 | yamllint: 13 | name: yamllint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | - uses: karancode/yamllint-github-action@master 18 | with: 19 | # fail on warnings and errors 20 | yamllint_strict: true 21 | yamllint_config_filepath: ".yamllint.yml" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | !dev.sh 3 | !dev-helm.sh 4 | !prod.sh 5 | !k8s-jobs/install-ingress.sh 6 | RT_SiteConfig.pm 7 | *.pem 8 | *.key 9 | !dev.sh 10 | !prod.sh 11 | !logs_prod.sh 12 | !bash_functions.sh 13 | !restart_prod.sh 14 | !cron_entrypoint.sh 15 | Caddyfile 16 | crontab 17 | /certs/* 18 | !/certs/.keep 19 | msmtp/* 20 | !msmtp/.keep 21 | getmail/* 22 | !getmail/.keep 23 | gpg/* 24 | !gpg/.keep 25 | shredder/*.sql 26 | *.secret 27 | *.env 28 | .env 29 | *.pgpass 30 | *.json 31 | docker-compose.override.yml 32 | *.patch 33 | # Added by goreleaser init: 34 | dist/ 35 | -------------------------------------------------------------------------------- /helm/templates/pvc-postgres.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.postgres.enabled -}} 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: rt-postgres-data 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | spec: 9 | accessModes: 10 | - {{ .Values.pvc.postgresData.accessMode }} 11 | resources: 12 | requests: 13 | storage: {{ .Values.pvc.postgresData.size }} 14 | {{- if .Values.pvc.postgresData.storageClass }} 15 | storageClassName: {{ .Values.pvc.postgresData.storageClass }} 16 | {{- end }} 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /helm/templates/route.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.route.enabled -}} 2 | kind: Route 3 | apiVersion: route.openshift.io/v1 4 | metadata: 5 | name: rt-route 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | {{- with .Values.route.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | host: {{ .Values.route.hostName }} 14 | tls: 15 | termination: edge 16 | insecureEdgeTerminationPolicy: None 17 | port: 18 | targetPort: http 19 | to: 20 | kind: Service 21 | name: rt-caddy 22 | {{- end }} -------------------------------------------------------------------------------- /helm/templates/service-rt.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: rt-caddy 5 | labels: 6 | {{- include "request-tracker.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.caddy.service.type }} 9 | ports: 10 | - port: {{ .Values.caddy.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: rt 14 | - port: {{ .Values.caddy.service.mailgatePort }} 15 | targetPort: mailgate 16 | protocol: TCP 17 | name: mailgate 18 | selector: 19 | {{- include "request-tracker.selectorLabels" . | nindent 4 }} 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: docker 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /Caddyfile.example: -------------------------------------------------------------------------------- 1 | { 2 | # debug 3 | admin off 4 | auto_https off 5 | } 6 | 7 | # healthchecks 8 | :1337 { 9 | respond "OK" 200 10 | } 11 | 12 | # mailgate 13 | :8080 { 14 | log 15 | reverse_proxy rt:9000 { 16 | transport fastcgi 17 | } 18 | } 19 | 20 | # request tracker 21 | :443 { 22 | log 23 | tls /certs/pub.pem /certs/priv.pem 24 | 25 | # Block access to the unauth mail gateway endpoint 26 | # we have a seperate mailgate server for that 27 | @blocked path /REST/1.0/NoAuth/mail-gateway 28 | respond @blocked "Nope" 403 29 | 30 | reverse_proxy rt:9000 { 31 | transport fastcgi 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /helm/templates/service-postgres.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.postgres.enabled -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: rt-db 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | app.kubernetes.io/component: postgres 9 | spec: 10 | type: {{ .Values.postgres.service.type }} 11 | ports: 12 | - port: {{ .Values.postgres.service.port }} 13 | targetPort: postgres 14 | protocol: TCP 15 | name: postgres 16 | selector: 17 | {{- include "request-tracker.selectorLabels" . | nindent 4 }} 18 | app.kubernetes.io/component: postgres 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euf -o pipefail 4 | 5 | export DOCKER_BUILDKIT=1 6 | export COMPOSE_DOCKER_CLI_BUILD=1 7 | 8 | DIR="${BASH_SOURCE%/*}" 9 | if [[ ! -d "${DIR}" ]]; then DIR="${PWD}"; fi 10 | . "${DIR}/bash_functions.sh" 11 | 12 | check_files 13 | check_dev_files 14 | 15 | fix_file_perms 16 | 17 | docker compose -f docker-compose.yml -f docker-compose.dev.yml stop 18 | docker compose -f docker-compose.yml -f docker-compose.dev.yml rm -f 19 | docker compose -f docker-compose.yml -f docker-compose.dev.yml --progress=plain build --pull 20 | docker compose -f docker-compose.yml -f docker-compose.dev.yml up --remove-orphans 21 | -------------------------------------------------------------------------------- /dev-helm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euf -o pipefail 4 | 5 | echo "uninstalling old stuff" 6 | kubectl delete job --all --ignore-not-found 7 | helm uninstall --ignore-not-found rt 8 | kubectl delete secret --all --ignore-not-found 9 | echo "sleeping 15 seconds to let things settle" 10 | sleep 15 11 | echo "installing new stuff" 12 | kubectl create secret generic rt-db-creds \ 13 | --from-literal=dbname=rt \ 14 | --from-literal=username=rt \ 15 | --from-literal=password='rt' 16 | helm install rt helm/ 17 | echo "sleeping 2 minutes to let the database come up" 18 | sleep 120 19 | echo "initializing the database" 20 | kubectl apply -f k8s-jobs/db-init.yaml 21 | echo "done" 22 | kubectl get pods 23 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Create chart name and version as used by the chart label. 3 | */}} 4 | {{- define "request-tracker.chart" -}} 5 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Common labels 10 | */}} 11 | {{- define "request-tracker.labels" -}} 12 | helm.sh/chart: {{ include "request-tracker.chart" . }} 13 | {{ include "request-tracker.selectorLabels" . }} 14 | {{- if .Chart.AppVersion }} 15 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 16 | {{- end }} 17 | app.kubernetes.io/managed-by: {{ .Release.Service }} 18 | {{- end }} 19 | 20 | {{/* 21 | Selector labels 22 | */}} 23 | {{- define "request-tracker.selectorLabels" -}} 24 | app.kubernetes.io/name: rt 25 | app.kubernetes.io/instance: {{ .Release.Name }} 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /.github/workflows/hadolint.yml: -------------------------------------------------------------------------------- 1 | name: hadolint 2 | on: 3 | push: 4 | paths: 5 | - "**/Dockerfile" 6 | pull_request: 7 | workflow_dispatch: 8 | permissions: 9 | contents: read 10 | jobs: 11 | hadolint: 12 | name: hadolint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: hadolint/hadolint-action@v3.3.0 17 | with: 18 | dockerfile: Dockerfile 19 | # DL3007: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag 20 | # DL3018: Pin versions in apk add. Instead of `apk add ` use `apk add =` 21 | # DL3008: Pin versions in apt get install. Instead of `apt-get install ` use `apt-get install =` 22 | # DL3003: Use WORKDIR to switch to a directory 23 | ignore: DL3007,DL3008,DL3018,DL3003 24 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge dependabot updates 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: write 10 | 11 | jobs: 12 | 13 | dependabot-merge: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | if: github.event.pull_request.user.login == 'dependabot[bot]' && startsWith(github.repository, 'firefart/') 18 | 19 | steps: 20 | - name: Dependabot metadata 21 | id: metadata 22 | uses: dependabot/fetch-metadata@v2.4.0 23 | with: 24 | github-token: "${{ secrets.GITHUB_TOKEN }}" 25 | 26 | - name: Enable auto-merge for Dependabot PRs 27 | # Only if version bump is not a major version change 28 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} 29 | run: gh pr merge --auto --merge "$PR_URL" 30 | env: 31 | PR_URL: ${{github.event.pull_request.html_url}} 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | -------------------------------------------------------------------------------- /crontab.example: -------------------------------------------------------------------------------- 1 | # do NOT use quotes here! 2 | MAILTO=user@user.com 3 | 4 | SHELL=/bin/sh 5 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 6 | 7 | # Example of job definition: 8 | # .---------------- minute (0 - 59) 9 | # | .------------- hour (0 - 23) 10 | # | | .---------- day of month (1 - 31) 11 | # | | | .------- month (1 - 12) OR jan,feb,mar,apr ... 12 | # | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat 13 | # | | | | | 14 | # * * * * * user-name command to be executed 15 | 16 | # * * * * * root date >/proc/1/fd/1 2>/proc/1/fd/2 17 | 18 | # clean sessions 19 | 0 0 * * * rt /opt/rt/sbin/rt-clean-sessions 20 | 21 | # refresh full text index 22 | 0 * * * * rt /opt/rt/sbin/rt-fulltext-indexer --quiet 2>&1 | grep -v "Words longer than 2047 characters are ignored" | grep -v "word is too long to be indexed" 23 | 24 | # get mails 25 | * * * * * rt /usr/bin/getmail --rcfile=getmailrc -g /getmail 26 | 27 | # email dashboards 28 | 0 * * * * rt /opt/rt/sbin/rt-email-dashboards 29 | -------------------------------------------------------------------------------- /helm/templates/hpa-rt.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rt.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: rt 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: rt 13 | minReplicas: {{ .Values.rt.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.rt.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.rt.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.rt.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.rt.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.rt.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /helm/templates/hpa-postgres.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.postgres.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: rt-db 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: rt-db 13 | minReplicas: {{ .Values.postgres.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.postgres.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.postgres.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.postgres.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.postgres.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.postgres.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /helm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: rt-ingress 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | {{- with .Values.ingress.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | {{- with .Values.ingress.className }} 14 | ingressClassName: {{ . }} 15 | {{- end }} 16 | {{- if .Values.ingress.tls }} 17 | tls: 18 | {{- range .Values.ingress.tls }} 19 | - hosts: 20 | {{- range .hosts }} 21 | - {{ . | quote }} 22 | {{- end }} 23 | secretName: {{ .secretName }} 24 | {{- end }} 25 | {{- end }} 26 | rules: 27 | {{- range .Values.ingress.hosts }} 28 | - host: {{ .host | quote }} 29 | http: 30 | paths: 31 | {{- range .paths }} 32 | - path: {{ .path }} 33 | {{- with .pathType }} 34 | pathType: {{ . }} 35 | {{- end }} 36 | backend: 37 | service: 38 | name: rt-caddy 39 | port: 40 | number: {{ $.Values.caddy.service.port }} 41 | {{- end }} 42 | {{- end }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | before: 12 | hooks: 13 | # You may remove this if you don't use go modules. 14 | - go mod tidy 15 | # you may remove this if you don't need go generate 16 | - go generate ./... 17 | 18 | builds: 19 | - env: 20 | - CGO_ENABLED=0 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | 26 | archives: 27 | - formats: [tar.gz] 28 | # this name template makes the OS and Arch compatible with the results of `uname`. 29 | name_template: >- 30 | {{ .ProjectName }}_ 31 | {{- title .Os }}_ 32 | {{- if eq .Arch "amd64" }}x86_64 33 | {{- else if eq .Arch "386" }}i386 34 | {{- else }}{{ .Arch }}{{ end }} 35 | {{- if .Arm }}v{{ .Arm }}{{ end }} 36 | # use zip for windows archives 37 | format_overrides: 38 | - goos: windows 39 | formats: [zip] 40 | 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - "^docs:" 46 | - "^test:" 47 | -------------------------------------------------------------------------------- /RT_SiteConfig.pm.example: -------------------------------------------------------------------------------- 1 | ### Base configuration ### 2 | Set($rtname, 'rt'); 3 | Set($WebDomain, 'localhost'); 4 | Set($WebPort, 443); 5 | Set($CanonicalizeRedirectURLs, 1); 6 | Set($CanonicalizeURLsInFeeds, 1); 7 | 8 | Plugin('RT::Extension::MergeUsers'); 9 | Plugin('RT::Extension::TerminalTheme'); 10 | 11 | ### Database connection ### 12 | Set($DatabaseType, 'Pg' ); 13 | Set($DatabaseHost, 'db'); 14 | Set($DatabasePort, '5432'); 15 | Set($DatabaseUser, 'rt'); 16 | Set($DatabasePassword, 'password'); 17 | Set($DatabaseName, 'rt'); 18 | Set($DatabaseAdmin, "rt"); 19 | 20 | Set($SendmailPath, '/usr/bin/msmtp'); 21 | 22 | ### GnuPG configuration ### 23 | Set(%GnuPG, 24 | Enable => 1, 25 | GnuPG => 'gpg', 26 | Passphrase => undef, 27 | OutgoingMessagesFormat => 'RFC' 28 | ); 29 | 30 | Set(%GnuPGOptions, 31 | homedir => '/opt/rt/var/data/gpg', 32 | passphrase => 'PASSPHRASE', 33 | keyserver => 'hkps://keys.openpgp.org', 34 | 'auto-key-retrieve' => undef, 35 | 'keyserver-options' => 'timeout=20', 36 | 'auto-key-locate' => 'keyserver', 37 | ); 38 | 39 | ### SMIME configuration ### 40 | Set(%SMIME, 41 | Enable => 1, 42 | AcceptUntrustedCAs => 1, 43 | OpenSSL => '/usr/bin/openssl', 44 | Keyring => '/opt/rt/var/data/smime', 45 | CAPath => '/opt/rt/var/data/smime/signing-ca.pem', 46 | Passphrase => { 47 | 'user@user.com' => 'PASSPHRASE', 48 | '' => 'fallback', 49 | }, 50 | ); 51 | 52 | 1; 53 | -------------------------------------------------------------------------------- /helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}:{{ $.Values.caddy.service.port }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.caddy.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services rt) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.caddy.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w rt' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} rt --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.caddy.service.port }} 17 | {{- else if contains "ClusterIP" .Values.caddy.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name=rt,app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rt: 3 | build: 4 | args: 5 | ADDITIONAL_CPANM_ARGS: "-n" # disable tests in dev to speed up builds 6 | restart: "no" 7 | deploy: 8 | mode: replicated 9 | replicas: 1 10 | depends_on: 11 | db: 12 | condition: service_healthy 13 | restart: true 14 | 15 | cron: 16 | build: 17 | args: 18 | ADDITIONAL_CPANM_ARGS: "-n" # disable tests in dev to speed up builds 19 | restart: "no" 20 | depends_on: 21 | db: 22 | condition: service_healthy 23 | restart: true 24 | 25 | caddy: 26 | restart: "no" 27 | 28 | db: 29 | image: postgres:latest 30 | restart: "no" 31 | environment: 32 | POSTGRES_DB: rt 33 | POSTGRES_USER: rt 34 | POSTGRES_PASSWORD: password 35 | healthcheck: 36 | test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] 37 | interval: 30s 38 | timeout: 10s 39 | retries: 5 40 | start_period: 60s 41 | volumes: 42 | - vol_db:/var/lib/postgresql/data 43 | ports: 44 | - "127.0.0.1:5432:5432" 45 | networks: 46 | - net 47 | 48 | pgadmin: 49 | image: dpage/pgadmin4:latest 50 | restart: "no" 51 | ports: 52 | - "127.0.0.1:8888:80" 53 | environment: 54 | PGADMIN_LISTEN_ADDRESS: 0.0.0.0 55 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-root@root.com} 56 | PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password 57 | PGADMIN_DISABLE_POSTFIX: disable 58 | healthcheck: 59 | test: ["CMD", "wget", "-O", "-", "http://127.0.0.1:80/misc/ping"] 60 | interval: 10s 61 | timeout: 10s 62 | start_period: 160s 63 | retries: 3 64 | depends_on: 65 | db: 66 | condition: service_healthy 67 | restart: true 68 | secrets: 69 | - pgadmin_password 70 | volumes: 71 | - vol_pgadmin:/var/lib/pgadmin 72 | networks: 73 | - net 74 | 75 | secrets: 76 | pgadmin_password: 77 | file: ./pgadmin_password.secret 78 | 79 | volumes: 80 | vol_pgadmin: 81 | vol_db: 82 | -------------------------------------------------------------------------------- /bash_functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euf -o pipefail 4 | 5 | function check_files() { 6 | # check for needed config files 7 | # these are mounted using docker-compose and are 8 | # required by the setup 9 | if [[ ! -f ./RT_SiteConfig.pm ]]; then 10 | echo "RT_SiteConfig.pm does not exist. Please see RT_SiteConfig.pm.example for an example configuration." 11 | exit 1 12 | fi 13 | 14 | if [[ ! -f ./Caddyfile ]]; then 15 | echo "Caddyfile does not exist. Please see Caddyfile.example for an example configuration." 16 | exit 1 17 | fi 18 | 19 | if [[ ! -f ./msmtp/msmtp.conf ]]; then 20 | echo "./msmtp/msmtp.conf does not exist. Please see msmtp.conf.example for an example configuration." 21 | exit 1; 22 | fi 23 | 24 | if [[ ! -f ./crontab ]]; then 25 | echo "./crontab does not exist. Please see crontab.example for an example configuration." 26 | exit 1 27 | fi 28 | 29 | if [[ ! -f ./getmail/getmailrc ]]; then 30 | echo "./getmail/getmailrc does not exist. Please see getmailrc.example for an example configuration." 31 | exit 1 32 | fi 33 | } 34 | 35 | function check_dev_files() { 36 | if [[ ! -f ./pgadmin_password.secret ]]; then 37 | echo "./pgadmin_password.secret does not exist. Please set a password." 38 | exit 1 39 | fi 40 | 41 | if [[ ! -f ./certs/pub.pem ]]; then 42 | echo "./certs/pub.pem does not exist. Please see Readme.md if you want to create a self signed certificate." 43 | exit 1 44 | fi 45 | 46 | if [[ ! -f ./certs/priv.pem ]]; then 47 | echo "./certs/priv.pem does not exist. Please see Readme.md if you want to create a self signed certificate." 48 | exit 1 49 | fi 50 | } 51 | 52 | function fix_file_perms() { 53 | # needed for the gpg and smime stuff 54 | # id 1000 is the rt user inside the docker image 55 | chown -R 1000:1000 ./cron 56 | chown -R 1000:1000 ./gpg 57 | chown -R 1000:1000 ./smime 58 | chown -R 1000:1000 ./shredder 59 | 60 | chmod 0700 ./cron 61 | chmod 0700 ./gpg 62 | chmod 0700 ./smime 63 | chmod 0700 ./shredder 64 | 65 | find ./cron -type f -exec chmod 0600 {} \; 66 | find ./gpg -type f -exec chmod 0600 {} \; 67 | find ./smime -type f -exec chmod 0600 {} \; 68 | find ./shredder -type f -exec chmod 0600 {} \; 69 | } 70 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-app: &default-app 2 | build: 3 | context: . 4 | image: firefart/requesttracker:latest 5 | restart: unless-stopped 6 | configs: 7 | - source: rt_site_config 8 | target: /opt/rt/etc/RT_SiteConfig.pm 9 | - source: msmtp 10 | target: /etc/msmtprc 11 | - source: getmail 12 | target: /getmail/getmailrc 13 | - source: crontab 14 | target: /crontab # needed as docker compose mounts the config as a bind mount and the uid parameter is not working here 15 | volumes: 16 | - ./msmtp/:/msmtp:ro 17 | - ./gpg/:/opt/rt/var/data/gpg 18 | - ./smime/:/opt/rt/var/data/smime:ro 19 | - ./shredder/:/opt/rt/var/data/RT-Shredder 20 | - /etc/localtime:/etc/localtime:ro 21 | - ./cron/:/cron 22 | # make the host available inside the image 23 | extra_hosts: 24 | - "host.docker.internal:host-gateway" 25 | networks: 26 | - net 27 | 28 | services: 29 | rt: 30 | <<: *default-app 31 | hostname: rt 32 | deploy: 33 | mode: replicated 34 | replicas: 5 35 | endpoint_mode: vip 36 | 37 | cron: 38 | <<: *default-app 39 | hostname: cron 40 | # the cron daemon needs to run as root 41 | user: root 42 | command: ["/root/cron_entrypoint.sh"] 43 | # disable the healthcheck from the main dockerfile 44 | healthcheck: 45 | test: ["CMD", "pidof", "cron"] 46 | interval: 10s 47 | timeout: 10s 48 | retries: 3 49 | depends_on: 50 | # needs to be up so we can use mailgate from the cron container 51 | rt: 52 | condition: service_healthy 53 | restart: true 54 | # we send rt-mailgate over to caddy 55 | caddy: 56 | condition: service_healthy 57 | restart: true 58 | 59 | caddy: 60 | image: caddy:latest 61 | hostname: caddy 62 | restart: unless-stopped 63 | ports: 64 | - "0.0.0.0:443:443" 65 | - "127.0.0.1:8080:8080" # expose mailgate vhost to host 66 | configs: 67 | - source: caddyfile 68 | target: /etc/caddy/Caddyfile 69 | volumes: 70 | - ./certs/:/certs/:ro 71 | - /etc/localtime:/etc/localtime:ro 72 | - vol_caddy_data:/data 73 | - vol_caddy_config:/config 74 | healthcheck: 75 | test: ["CMD", "wget", "-O", "-", "-q", "http://127.0.0.1:1337/"] 76 | interval: 10s 77 | timeout: 10s 78 | retries: 3 79 | depends_on: 80 | rt: 81 | condition: service_healthy 82 | restart: true 83 | networks: 84 | - net 85 | 86 | configs: 87 | caddyfile: 88 | file: ./Caddyfile 89 | rt_site_config: 90 | file: ./RT_SiteConfig.pm 91 | msmtp: 92 | file: ./msmtp/msmtp.conf 93 | getmail: 94 | file: ./getmail/getmailrc 95 | crontab: 96 | file: ./crontab 97 | 98 | networks: 99 | net: 100 | driver: bridge 101 | driver_opts: 102 | com.docker.network.bridge.name: br_rt 103 | 104 | volumes: 105 | vol_caddy_data: 106 | vol_caddy_config: 107 | -------------------------------------------------------------------------------- /helm/templates/pvc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.pvc.gpg.enabled -}} 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: rt-gpg 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | spec: 9 | accessModes: 10 | - {{ .Values.pvc.gpg.accessMode }} 11 | resources: 12 | requests: 13 | storage: {{ .Values.pvc.gpg.size }} 14 | {{- if .Values.pvc.gpg.storageClass }} 15 | storageClassName: {{ .Values.pvc.gpg.storageClass }} 16 | {{- end }} 17 | {{- end }} 18 | 19 | {{ if .Values.pvc.smime.enabled -}} 20 | --- 21 | apiVersion: v1 22 | kind: PersistentVolumeClaim 23 | metadata: 24 | name: rt-smime 25 | labels: 26 | {{- include "request-tracker.labels" . | nindent 4 }} 27 | spec: 28 | accessModes: 29 | - {{ .Values.pvc.smime.accessMode }} 30 | resources: 31 | requests: 32 | storage: {{ .Values.pvc.smime.size }} 33 | {{- if .Values.pvc.smime.storageClass }} 34 | storageClassName: {{ .Values.pvc.smime.storageClass }} 35 | {{- end }} 36 | {{- end }} 37 | 38 | {{ if .Values.pvc.shredder.enabled -}} 39 | --- 40 | apiVersion: v1 41 | kind: PersistentVolumeClaim 42 | metadata: 43 | name: rt-shredder 44 | labels: 45 | {{- include "request-tracker.labels" . | nindent 4 }} 46 | spec: 47 | accessModes: 48 | - {{ .Values.pvc.shredder.accessMode }} 49 | resources: 50 | requests: 51 | storage: {{ .Values.pvc.shredder.size }} 52 | {{- if .Values.pvc.shredder.storageClass }} 53 | storageClassName: {{ .Values.pvc.shredder.storageClass }} 54 | {{- end }} 55 | {{- end }} 56 | 57 | {{ if .Values.pvc.cron.enabled -}} 58 | --- 59 | apiVersion: v1 60 | kind: PersistentVolumeClaim 61 | metadata: 62 | name: rt-cron 63 | labels: 64 | {{- include "request-tracker.labels" . | nindent 4 }} 65 | spec: 66 | accessModes: 67 | - {{ .Values.pvc.cron.accessMode }} 68 | resources: 69 | requests: 70 | storage: {{ .Values.pvc.cron.size }} 71 | {{- if .Values.pvc.cron.storageClass }} 72 | storageClassName: {{ .Values.pvc.cron.storageClass }} 73 | {{- end }} 74 | {{- end }} 75 | 76 | {{ if .Values.pvc.caddyData.enabled -}} 77 | --- 78 | apiVersion: v1 79 | kind: PersistentVolumeClaim 80 | metadata: 81 | name: rt-caddy-data 82 | labels: 83 | {{- include "request-tracker.labels" . | nindent 4 }} 84 | spec: 85 | accessModes: 86 | - {{ .Values.pvc.caddyData.accessMode }} 87 | resources: 88 | requests: 89 | storage: {{ .Values.pvc.caddyData.size }} 90 | {{- if .Values.pvc.caddyData.storageClass }} 91 | storageClassName: {{ .Values.pvc.caddyData.storageClass }} 92 | {{- end }} 93 | {{- end }} 94 | {{ if .Values.pvc.caddyConfig.enabled -}} 95 | --- 96 | apiVersion: v1 97 | kind: PersistentVolumeClaim 98 | metadata: 99 | name: rt-caddy-config 100 | labels: 101 | {{- include "request-tracker.labels" . | nindent 4 }} 102 | spec: 103 | accessModes: 104 | - {{ .Values.pvc.caddyConfig.accessMode }} 105 | resources: 106 | requests: 107 | storage: {{ .Values.pvc.caddyConfig.size }} 108 | {{- if .Values.pvc.caddyConfig.storageClass }} 109 | storageClassName: {{ .Values.pvc.caddyConfig.storageClass }} 110 | {{- end }} 111 | {{- end }} 112 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Images 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - Dockerfile 9 | - .github/workflows/docker.yml 10 | - cron_entrypoint.sh 11 | # only build without pushing on PRs 12 | pull_request: 13 | paths: 14 | - Dockerfile 15 | - .github/workflows/docker.yml 16 | - cron_entrypoint.sh 17 | workflow_dispatch: 18 | schedule: 19 | - cron: "0 0 * * *" 20 | 21 | permissions: 22 | contents: read 23 | 24 | # only allow one build at a time per branch 25 | concurrency: 26 | group: ci-${{ github.ref }} 27 | cancel-in-progress: true 28 | 29 | env: 30 | DO_PUSH: ${{ github.ref == 'refs/heads/main' }} # do not push on PRs 31 | 32 | jobs: 33 | images: 34 | timeout-minutes: 500 35 | runs-on: ubuntu-latest 36 | 37 | strategy: 38 | matrix: 39 | versions: 40 | - rt: "5.0.8" 41 | rtir: "5.0.8" 42 | - rt: "5.0.9" 43 | rtir: "5.0.8" 44 | create_major: "true" # only enable on the latest version of a major 45 | - rt: "6.0.0" 46 | rtir: "5.0.8" 47 | - rt: "6.0.1" 48 | rtir: "6.0.1" 49 | - rt: "6.0.2" 50 | rtir: "6.0.1" 51 | latest_tag: "true" # point the latest tag to this version 52 | create_major: "true" # only enable on the latest version of a major 53 | 54 | steps: 55 | - name: checkout sources 56 | uses: actions/checkout@v6 57 | 58 | - name: Set up QEMU 59 | uses: docker/setup-qemu-action@v3 60 | 61 | - name: Set up Docker Buildx 62 | uses: docker/setup-buildx-action@v3 63 | id: buildx 64 | with: 65 | install: true 66 | 67 | - name: Docker meta 68 | id: meta 69 | uses: docker/metadata-action@v5 70 | with: 71 | images: firefart/requesttracker 72 | flavor: | 73 | # disable setting the latest tag by default 74 | latest=false 75 | tags: | 76 | # Enable latest tag if set 77 | type=raw,value=latest,enable=${{ matrix.versions.latest_tag == 'true' }} 78 | # Create a tag for the major version 79 | type=semver,pattern={{major}},value=${{ matrix.versions.rt }},enable=${{ matrix.versions.create_major == 'true' }} 80 | # Create a tag for the full version 81 | type=raw,value=${{ matrix.versions.rt }} 82 | type=schedule,pattern={{date 'YYYYMMDD'}},prefix=nightly-${{ matrix.versions.rt }}- 83 | 84 | - name: Login to Docker Hub 85 | uses: docker/login-action@v3.6.0 86 | if: env.DO_PUSH == 'true' 87 | with: 88 | username: ${{ secrets.DOCKERHUB_USERNAME }} 89 | password: ${{ secrets.DOCKERHUB_TOKEN }} 90 | 91 | - name: Build and push 92 | uses: docker/build-push-action@v6 93 | with: 94 | build-args: | 95 | RT_VERSION=${{ matrix.versions.rt }} 96 | RTIR_VERSION=${{ matrix.versions.rtir }} 97 | context: . 98 | file: Dockerfile 99 | platforms: linux/amd64,linux/arm64 100 | sbom: true 101 | provenance: mode=max 102 | push: ${{ env.DO_PUSH }} 103 | tags: ${{ steps.meta.outputs.tags }} 104 | labels: ${{ steps.meta.outputs.labels }} 105 | -------------------------------------------------------------------------------- /k8s-jobs/db-init.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: db-init-job 5 | spec: 6 | ttlSecondsAfterFinished: 3600 # Clean up the job after 1 hour 7 | template: 8 | spec: 9 | restartPolicy: Never 10 | initContainers: 11 | - name: generate-config 12 | image: "hairyhenderson/gomplate:stable-alpine" 13 | imagePullPolicy: IfNotPresent 14 | securityContext: 15 | readOnlyRootFilesystem: true 16 | runAsNonRoot: true 17 | allowPrivilegeEscalation: false 18 | runAsUser: 65534 # nobody user 19 | runAsGroup: 65534 # nobody group 20 | command: 21 | - gomplate 22 | - -f 23 | - /templates/RT_SiteConfig.pm 24 | - -o 25 | - /output/RT_SiteConfig.pm 26 | resources: 27 | limits: 28 | cpu: 100m 29 | memory: 128Mi 30 | requests: 31 | cpu: 50m 32 | memory: 64Mi 33 | env: 34 | - name: RT_DB_NAME 35 | valueFrom: 36 | secretKeyRef: 37 | name: rt-db-creds 38 | key: dbname 39 | - name: RT_DB_USER 40 | valueFrom: 41 | secretKeyRef: 42 | name: rt-db-creds 43 | key: username 44 | - name: RT_DB_PASS 45 | valueFrom: 46 | secretKeyRef: 47 | name: rt-db-creds 48 | key: password 49 | volumeMounts: 50 | - name: rt-config-template 51 | mountPath: /templates/RT_SiteConfig.pm 52 | subPath: RT_SiteConfig.pm 53 | readOnly: true 54 | - name: rt-config 55 | mountPath: /output 56 | readOnly: false 57 | containers: 58 | - name: db-init 59 | securityContext: 60 | readOnlyRootFilesystem: true 61 | runAsNonRoot: true 62 | runAsUser: 1000 # the user id of the rt user in the container 63 | runAsGroup: 1000 # the group id of the rt user in the container 64 | allowPrivilegeEscalation: false 65 | resources: 66 | limits: 67 | cpu: 500m 68 | memory: 512Mi 69 | requests: 70 | cpu: 100m 71 | memory: 128Mi 72 | image: "firefart/requesttracker:latest" # adjest the tag as needed 73 | imagePullPolicy: "Always" 74 | command: ["/opt/rt/sbin/rt-setup-database"] 75 | args: ["--action", "init", "--skip-create"] 76 | workingDir: /opt/rt 77 | volumeMounts: 78 | - name: rt-config 79 | mountPath: /opt/rt/etc/RT_SiteConfig.pm 80 | subPath: RT_SiteConfig.pm 81 | readOnly: true 82 | - name: rt-gpg 83 | mountPath: /opt/rt/var/data/gpg 84 | readOnly: false 85 | - name: rt-smime 86 | mountPath: /opt/rt/var/data/smime 87 | readOnly: true 88 | - name: tmp 89 | mountPath: /tmp 90 | readOnly: false 91 | volumes: 92 | - name: rt-config-template 93 | configMap: 94 | name: rt-config 95 | items: 96 | - key: rtSiteConfig 97 | path: RT_SiteConfig.pm 98 | - name: rt-config 99 | emptyDir: {} 100 | - name: rt-gpg 101 | persistentVolumeClaim: 102 | claimName: rt-gpg 103 | - name: rt-smime 104 | persistentVolumeClaim: 105 | claimName: rt-smime 106 | - name: tmp 107 | emptyDir: {} 108 | -------------------------------------------------------------------------------- /k8s-jobs/db-update.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: db-update-job 5 | spec: 6 | ttlSecondsAfterFinished: 3600 # Clean up the job after 1 hour 7 | template: 8 | spec: 9 | restartPolicy: Never 10 | initContainers: 11 | - name: generate-config 12 | image: "hairyhenderson/gomplate:stable-alpine" 13 | imagePullPolicy: IfNotPresent 14 | securityContext: 15 | readOnlyRootFilesystem: true 16 | runAsNonRoot: true 17 | allowPrivilegeEscalation: false 18 | runAsUser: 65534 # nobody user 19 | runAsGroup: 65534 # nobody group 20 | command: 21 | - gomplate 22 | - -f 23 | - /templates/RT_SiteConfig.pm 24 | - -o 25 | - /output/RT_SiteConfig.pm 26 | resources: 27 | limits: 28 | cpu: 100m 29 | memory: 128Mi 30 | requests: 31 | cpu: 50m 32 | memory: 64Mi 33 | env: 34 | - name: RT_DB_NAME 35 | valueFrom: 36 | secretKeyRef: 37 | name: rt-db-creds 38 | key: dbname 39 | - name: RT_DB_USER 40 | valueFrom: 41 | secretKeyRef: 42 | name: rt-db-creds 43 | key: username 44 | - name: RT_DB_PASS 45 | valueFrom: 46 | secretKeyRef: 47 | name: rt-db-creds 48 | key: password 49 | volumeMounts: 50 | - name: rt-config-template 51 | mountPath: /templates/RT_SiteConfig.pm 52 | subPath: RT_SiteConfig.pm 53 | readOnly: true 54 | - name: rt-config 55 | mountPath: /output 56 | readOnly: false 57 | containers: 58 | - name: db-update 59 | securityContext: 60 | readOnlyRootFilesystem: true 61 | runAsNonRoot: true 62 | runAsUser: 1000 # the user id of the rt user in the container 63 | runAsGroup: 1000 # the group id of the rt user in the container 64 | allowPrivilegeEscalation: false 65 | resources: 66 | limits: 67 | cpu: 500m 68 | memory: 512Mi 69 | requests: 70 | cpu: 100m 71 | memory: 128Mi 72 | image: "firefart/requesttracker:latest" # adjest the tag as needed 73 | imagePullPolicy: "Always" 74 | command: ["/opt/rt/sbin/rt-setup-database"] 75 | args: ["--action", "upgrade", "--upgrade-from=4.4.2"] # adjust the version as needed 76 | workingDir: /opt/rt 77 | volumeMounts: 78 | - name: rt-config 79 | mountPath: /opt/rt/etc/RT_SiteConfig.pm 80 | subPath: RT_SiteConfig.pm 81 | readOnly: true 82 | - name: rt-gpg 83 | mountPath: /opt/rt/var/data/gpg 84 | readOnly: false 85 | - name: rt-smime 86 | mountPath: /opt/rt/var/data/smime 87 | readOnly: true 88 | - name: tmp 89 | mountPath: /tmp 90 | readOnly: false 91 | volumes: 92 | - name: rt-config-template 93 | configMap: 94 | name: rt-config 95 | items: 96 | - key: rtSiteConfig 97 | path: RT_SiteConfig.pm 98 | - name: rt-config 99 | emptyDir: {} 100 | - name: rt-gpg 101 | persistentVolumeClaim: 102 | claimName: rt-gpg 103 | - name: rt-smime 104 | persistentVolumeClaim: 105 | claimName: rt-smime 106 | - name: tmp 107 | emptyDir: {} 108 | -------------------------------------------------------------------------------- /helm/templates/jobs.yaml: -------------------------------------------------------------------------------- 1 | {{- range .Values.cronjobs }} 2 | --- 3 | apiVersion: batch/v1 4 | kind: CronJob 5 | metadata: 6 | name: "{{ printf "rt-%s" .name | trunc -63 | replace "_" "-" }}" 7 | labels: 8 | {{- include "request-tracker.labels" $ | nindent 4 }} 9 | spec: 10 | schedule: "{{ .schedule }}" 11 | concurrencyPolicy: {{ .concurrencyPolicy | default "Forbid" }} 12 | timeZone: {{ .timeZone | default "UTC" }} 13 | jobTemplate: 14 | spec: 15 | template: 16 | spec: 17 | serviceAccountName: rt 18 | securityContext: 19 | {{- toYaml .podSecurityContext | default $.Values.rt.podSecurityContext | nindent 12 }} 20 | containers: 21 | - name: "{{ printf "rt-%s" .name | trunc -63 | replace "_" "-" }}" 22 | securityContext: 23 | {{- toYaml (.securityContext | default $.Values.rt.securityContext) | nindent 14 }} 24 | image: "{{ .image.repository | default $.Values.rt.image.repository }}:{{ .image.tag | default $.Values.rt.image.tag }}" 25 | imagePullPolicy: {{ .image.pullPolicy | default $.Values.rt.image.pullPolicy }} 26 | command: 27 | {{- toYaml .command | nindent 14 }} 28 | {{- with .resources }} 29 | resources: 30 | {{- toYaml . | nindent 14 }} 31 | {{- end }} 32 | volumeMounts: 33 | - name: rt-config 34 | mountPath: /opt/rt/etc/RT_SiteConfig.pm 35 | subPath: RT_SiteConfig.pm 36 | readOnly: true 37 | - name: msmtp-config 38 | mountPath: /msmtp/msmtp.conf 39 | subPath: msmtp.conf 40 | readOnly: true 41 | - name: getmailrc-config 42 | mountPath: /getmailrc 43 | subPath: getmailrc 44 | readOnly: true 45 | - name: getmail-dir 46 | mountPath: /getmail 47 | readOnly: false 48 | - name: rt-gpg 49 | mountPath: /opt/rt/var/data/gpg 50 | readOnly: false 51 | - name: rt-smime 52 | mountPath: /opt/rt/var/data/smime 53 | readOnly: true 54 | - name: rt-shredder 55 | mountPath: /opt/rt/var/data/RT-Shredder 56 | readOnly: false 57 | - name: rt-cron 58 | mountPath: /cron 59 | readOnly: false 60 | {{- with $.Values.rt.volumeMounts }} 61 | {{- toYaml . | nindent 12 }} 62 | {{- end }} 63 | volumes: 64 | - name: rt-config 65 | configMap: 66 | name: rt-config 67 | items: 68 | - key: rtSiteConfig 69 | path: RT_SiteConfig.pm 70 | - name: msmtp-config 71 | configMap: 72 | name: rt-config 73 | items: 74 | - key: msmtp 75 | path: msmtp.conf 76 | - name: getmailrc-config 77 | configMap: 78 | name: rt-config 79 | items: 80 | - key: getmailrc 81 | path: getmailrc 82 | - name: getmail-dir 83 | emptyDir: {} 84 | - name: rt-gpg 85 | persistentVolumeClaim: 86 | claimName: rt-gpg 87 | - name: rt-smime 88 | persistentVolumeClaim: 89 | claimName: rt-smime 90 | - name: rt-shredder 91 | persistentVolumeClaim: 92 | claimName: rt-shredder 93 | - name: rt-cron 94 | persistentVolumeClaim: 95 | claimName: rt-cron 96 | {{- with $.Values.rt.volumes }} 97 | {{- toYaml . | nindent 12 }} 98 | {{- end }} 99 | restartPolicy: {{ .restartPolicy | default "Never" }} 100 | {{- end }} 101 | -------------------------------------------------------------------------------- /helm/templates/deployment-postgres.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.postgres.enabled -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: rt-db 6 | labels: 7 | {{- include "request-tracker.labels" . | nindent 4 }} 8 | app.kubernetes.io/component: postgres 9 | spec: 10 | {{- if not .Values.postgres.autoscaling.enabled }} 11 | replicas: {{ .Values.postgres.replicaCount }} 12 | {{- end }} 13 | selector: 14 | matchLabels: 15 | {{- include "request-tracker.selectorLabels" . | nindent 6 }} 16 | app.kubernetes.io/component: postgres 17 | template: 18 | metadata: 19 | {{- with .Values.podAnnotations }} 20 | annotations: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | labels: 24 | {{- include "request-tracker.labels" . | nindent 8 }} 25 | app.kubernetes.io/component: postgres 26 | {{- with .Values.podLabels }} 27 | {{- toYaml . | nindent 8 }} 28 | {{- end }} 29 | spec: 30 | {{- with .Values.postgres.imagePullSecrets }} 31 | imagePullSecrets: 32 | {{- toYaml . | nindent 8 }} 33 | {{- end }} 34 | serviceAccountName: rt 35 | {{- with .Values.postgres.podSecurityContext }} 36 | securityContext: 37 | {{- toYaml . | nindent 8 }} 38 | {{- end }} 39 | containers: 40 | - name: {{ .Chart.Name }} 41 | {{- with .Values.postgres.securityContext }} 42 | securityContext: 43 | {{- toYaml . | nindent 12 }} 44 | {{- end }} 45 | image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" 46 | imagePullPolicy: {{ .Values.postgres.image.pullPolicy }} 47 | ports: 48 | - name: postgres 49 | containerPort: 5432 50 | protocol: TCP 51 | livenessProbe: 52 | exec: 53 | command: 54 | - bash 55 | - -c 56 | - pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB} 57 | initialDelaySeconds: 30 58 | periodSeconds: 30 59 | timeoutSeconds: 10 60 | failureThreshold: 5 61 | readinessProbe: 62 | exec: 63 | command: 64 | - bash 65 | - -c 66 | - pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB} 67 | initialDelaySeconds: 30 68 | periodSeconds: 30 69 | timeoutSeconds: 10 70 | failureThreshold: 5 71 | {{- with .Values.postgres.resources }} 72 | resources: 73 | {{- toYaml . | nindent 12 }} 74 | {{- end }} 75 | env: 76 | - name: PGDATA 77 | value: /var/lib/postgresql/data/pgdata # subdirectory needed as initdb tries to chown the root of the volume 78 | - name: POSTGRES_DB_FILE 79 | value: /etc/secrets/dbname 80 | - name: POSTGRES_USER_FILE 81 | value: /etc/secrets/username 82 | - name: POSTGRES_PASSWORD_FILE 83 | value: /etc/secrets/password 84 | volumeMounts: 85 | - mountPath: /var/lib/postgresql/data 86 | name: postgres-data 87 | readOnly: false 88 | - mountPath: /var/run/postgresql 89 | name: postgres-run 90 | readOnly: false 91 | - name: secret-volume 92 | mountPath: /etc/secrets 93 | readOnly: true 94 | {{- with .Values.postgres.volumeMounts }} 95 | {{- toYaml . | nindent 12 }} 96 | {{- end }} 97 | volumes: 98 | - name: postgres-data 99 | persistentVolumeClaim: 100 | claimName: rt-postgres-data 101 | - name: postgres-run 102 | emptyDir: 103 | sizeLimit: 100Mi 104 | - name: secret-volume 105 | secret: 106 | secretName: rt-db-creds 107 | {{- with .Values.postgres.volumes }} 108 | {{- toYaml . | nindent 8 }} 109 | {{- end }} 110 | {{- with .Values.nodeSelector }} 111 | nodeSelector: 112 | {{- toYaml . | nindent 8 }} 113 | {{- end }} 114 | {{- with .Values.affinity }} 115 | affinity: 116 | {{- toYaml . | nindent 8 }} 117 | {{- end }} 118 | {{- with .Values.tolerations }} 119 | tolerations: 120 | {{- toYaml . | nindent 8 }} 121 | {{- end }} 122 | {{- end }} 123 | -------------------------------------------------------------------------------- /helm/templates/deployment-rt.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: rt 5 | labels: 6 | {{- include "request-tracker.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: rt 8 | spec: 9 | {{- if not .Values.rt.autoscaling.enabled }} 10 | replicas: {{ .Values.rt.replicaCount }} 11 | {{- end }} 12 | selector: 13 | matchLabels: 14 | {{- include "request-tracker.selectorLabels" . | nindent 6 }} 15 | app.kubernetes.io/component: rt 16 | template: 17 | metadata: 18 | {{- with .Values.podAnnotations }} 19 | annotations: 20 | {{- toYaml . | nindent 8 }} 21 | {{- end }} 22 | labels: 23 | {{- include "request-tracker.labels" . | nindent 8 }} 24 | app.kubernetes.io/component: rt 25 | {{- with .Values.podLabels }} 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | spec: 29 | {{- with .Values.rt.imagePullSecrets }} 30 | imagePullSecrets: 31 | {{- toYaml . | nindent 8 }} 32 | {{- end }} 33 | serviceAccountName: rt 34 | {{- with .Values.rt.podSecurityContext }} 35 | securityContext: 36 | {{- toYaml . | nindent 8 }} 37 | {{- end }} 38 | initContainers: 39 | {{- if .Values.postgres.enabled }} 40 | # ensure that the database is ready before starting RT 41 | - name: init-db 42 | image: busybox:1.37 43 | securityContext: 44 | readOnlyRootFilesystem: true 45 | runAsNonRoot: true 46 | allowPrivilegeEscalation: false 47 | runAsUser: 65534 # nobody user in the busybox image 48 | runAsGroup: 65534 # nobody user in the busybox image 49 | command: ['sh', '-c', "until nslookup rt-db.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for rt-db; sleep 2; done"] 50 | {{- with .Values.rt.resourcesInitContainer }} 51 | resources: 52 | {{- toYaml . | nindent 12 }} 53 | {{- end }} 54 | - name: wait-db 55 | image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" 56 | securityContext: 57 | readOnlyRootFilesystem: true 58 | runAsNonRoot: true 59 | allowPrivilegeEscalation: false 60 | runAsUser: 999 # postgres user in the postgres image 61 | runAsGroup: 999 # postgres group in the postgres image 62 | command: 63 | - bash 64 | - -c 65 | - until pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB} -h rt-db; do echo waiting for database; sleep 2; done; 66 | {{- with .Values.rt.resourcesInitContainer }} 67 | resources: 68 | {{- toYaml . | nindent 12 }} 69 | {{- end }} 70 | env: 71 | - name: POSTGRES_DB 72 | valueFrom: 73 | secretKeyRef: 74 | name: rt-db-creds 75 | key: dbname 76 | - name: POSTGRES_USER 77 | valueFrom: 78 | secretKeyRef: 79 | name: rt-db-creds 80 | key: username 81 | - name: POSTGRES_PASSWORD 82 | valueFrom: 83 | secretKeyRef: 84 | name: rt-db-creds 85 | key: password 86 | {{- end }} 87 | - name: generate-config 88 | image: "hairyhenderson/gomplate:stable-alpine" 89 | imagePullPolicy: IfNotPresent 90 | securityContext: 91 | readOnlyRootFilesystem: true 92 | runAsNonRoot: true 93 | allowPrivilegeEscalation: false 94 | runAsUser: 65534 # nobody user 95 | runAsGroup: 65534 # nobody group 96 | command: 97 | - gomplate 98 | - -f 99 | - /templates/RT_SiteConfig.pm 100 | - -o 101 | - /output/RT_SiteConfig.pm 102 | {{- with .Values.rt.resourcesInitContainer }} 103 | resources: 104 | {{- toYaml . | nindent 12 }} 105 | {{- end }} 106 | env: 107 | - name: RT_DB_NAME 108 | valueFrom: 109 | secretKeyRef: 110 | name: rt-db-creds 111 | key: dbname 112 | - name: RT_DB_USER 113 | valueFrom: 114 | secretKeyRef: 115 | name: rt-db-creds 116 | key: username 117 | - name: RT_DB_PASS 118 | valueFrom: 119 | secretKeyRef: 120 | name: rt-db-creds 121 | key: password 122 | volumeMounts: 123 | - name: rt-config-template 124 | mountPath: /templates/RT_SiteConfig.pm 125 | subPath: RT_SiteConfig.pm 126 | readOnly: true 127 | - name: rt-config 128 | mountPath: /output 129 | readOnly: false 130 | containers: 131 | - name: rt 132 | {{- with .Values.rt.securityContext }} 133 | securityContext: 134 | {{- toYaml . | nindent 12 }} 135 | {{- end }} 136 | image: "{{ .Values.rt.image.repository }}:{{ .Values.rt.image.tag }}" 137 | imagePullPolicy: {{ .Values.rt.image.pullPolicy }} 138 | livenessProbe: 139 | exec: 140 | command: 141 | - /bin/sh 142 | - -c 143 | - "REQUEST_METHOD=GET REQUEST_URI=/ SCRIPT_NAME=/ cgi-fcgi -connect localhost:{{ .Values.rt.port }} -bind || exit 1" 144 | initialDelaySeconds: 30 145 | periodSeconds: 10 146 | timeoutSeconds: 10 147 | failureThreshold: 3 148 | readinessProbe: 149 | exec: 150 | command: 151 | - /bin/sh 152 | - -c 153 | - "REQUEST_METHOD=GET REQUEST_URI=/ SCRIPT_NAME=/ cgi-fcgi -connect localhost:{{ .Values.rt.port }} -bind || exit 1" 154 | initialDelaySeconds: 5 155 | periodSeconds: 5 156 | timeoutSeconds: 10 157 | failureThreshold: 3 158 | {{- with .Values.rt.resources }} 159 | resources: 160 | {{- toYaml . | nindent 12 }} 161 | {{- end }} 162 | volumeMounts: 163 | - name: rt-config 164 | mountPath: /opt/rt/etc/RT_SiteConfig.pm 165 | subPath: RT_SiteConfig.pm 166 | readOnly: true 167 | - name: msmtp-config 168 | mountPath: /msmtp/msmtp.conf 169 | subPath: msmtp.conf 170 | readOnly: true 171 | - name: getmailrc-config 172 | mountPath: /getmail/getmailrc 173 | subPath: getmailrc 174 | readOnly: true 175 | - name: rt-gpg 176 | mountPath: /opt/rt/var/data/gpg 177 | readOnly: false 178 | - name: rt-smime 179 | mountPath: /opt/rt/var/data/smime 180 | readOnly: true 181 | - name: rt-shredder 182 | mountPath: /opt/rt/var/data/RT-Shredder 183 | readOnly: false 184 | - name: rt-cron 185 | mountPath: /cron 186 | readOnly: false 187 | - name: tmp 188 | mountPath: /tmp 189 | readOnly: false 190 | - name: mason-data 191 | mountPath: /opt/rt/var/mason_data/ 192 | readOnly: false 193 | {{- with .Values.rt.volumeMounts }} 194 | {{- toYaml . | nindent 12 }} 195 | {{- end }} 196 | - name: caddy 197 | {{- with .Values.caddy.securityContext }} 198 | securityContext: 199 | {{- toYaml . | nindent 12 }} 200 | {{- end }} 201 | image: "{{ .Values.caddy.image.repository }}:{{ .Values.caddy.image.tag }}" 202 | imagePullPolicy: {{ .Values.caddy.image.pullPolicy }} 203 | ports: 204 | - name: http 205 | containerPort: {{ .Values.caddy.ports.http }} 206 | protocol: TCP 207 | - name: mailgate 208 | containerPort: {{ .Values.caddy.ports.mailgate }} 209 | protocol: TCP 210 | - name: health 211 | containerPort: {{ .Values.caddy.ports.health }} 212 | protocol: TCP 213 | {{- with .Values.caddy.livenessProbe }} 214 | livenessProbe: 215 | {{- toYaml . | nindent 12 }} 216 | {{- end }} 217 | {{- with .Values.caddy.readinessProbe }} 218 | readinessProbe: 219 | {{- toYaml . | nindent 12 }} 220 | {{- end }} 221 | {{- with .Values.caddy.resources }} 222 | resources: 223 | {{- toYaml . | nindent 12 }} 224 | {{- end }} 225 | volumeMounts: 226 | - name: caddy-configfile 227 | mountPath: /etc/caddy/Caddyfile 228 | subPath: Caddyfile 229 | readOnly: true 230 | - name: caddy-data 231 | mountPath: /data 232 | readOnly: false 233 | - name: caddy-config 234 | mountPath: /config 235 | readOnly: false 236 | {{- with .Values.caddy.volumeMounts }} 237 | {{- toYaml . | nindent 12 }} 238 | {{- end }} 239 | volumes: 240 | - name: rt-config-template 241 | configMap: 242 | name: rt-config 243 | items: 244 | - key: rtSiteConfig 245 | path: RT_SiteConfig.pm 246 | - name: rt-config 247 | emptyDir: {} 248 | - name: msmtp-config 249 | configMap: 250 | name: rt-config 251 | items: 252 | - key: msmtp 253 | path: msmtp.conf 254 | - name: getmailrc-config 255 | configMap: 256 | name: rt-config 257 | items: 258 | - key: getmailrc 259 | path: getmailrc 260 | - name: rt-gpg 261 | persistentVolumeClaim: 262 | claimName: rt-gpg 263 | - name: rt-smime 264 | persistentVolumeClaim: 265 | claimName: rt-smime 266 | - name: rt-shredder 267 | persistentVolumeClaim: 268 | claimName: rt-shredder 269 | - name: rt-cron 270 | persistentVolumeClaim: 271 | claimName: rt-cron 272 | - name: tmp 273 | emptyDir: {} 274 | - name: mason-data 275 | emptyDir: {} 276 | {{- with .Values.rt.volumes }} 277 | {{- toYaml . | nindent 8 }} 278 | {{- end }} 279 | - name: caddy-configfile 280 | configMap: 281 | name: rt-config 282 | items: 283 | - key: caddyfile 284 | path: Caddyfile 285 | - name: caddy-data 286 | persistentVolumeClaim: 287 | claimName: rt-caddy-data 288 | - name: caddy-config 289 | persistentVolumeClaim: 290 | claimName: rt-caddy-config 291 | {{- with .Values.caddy.volumes }} 292 | {{- toYaml . | nindent 8 }} 293 | {{- end }} 294 | {{- with .Values.nodeSelector }} 295 | nodeSelector: 296 | {{- toYaml . | nindent 8 }} 297 | {{- end }} 298 | {{- with .Values.affinity }} 299 | affinity: 300 | {{- toYaml . | nindent 8 }} 301 | {{- end }} 302 | {{- with .Values.tolerations }} 303 | tolerations: 304 | {{- toYaml . | nindent 8 }} 305 | {{- end }} 306 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | rt: 2 | replicaCount: 1 3 | autoscaling: 4 | enabled: false 5 | minReplicas: 1 6 | maxReplicas: 10 7 | targetCPUUtilizationPercentage: 80 8 | targetMemoryUtilizationPercentage: 80 9 | 10 | image: 11 | repository: firefart/requesttracker 12 | pullPolicy: Always 13 | tag: "6.0.1" 14 | 15 | imagePullSecrets: [] 16 | 17 | securityContext: 18 | readOnlyRootFilesystem: true 19 | runAsNonRoot: true 20 | runAsUser: 1000 # the user id of the rt user in the container 21 | runAsGroup: 1000 # the group id of the rt user in the container 22 | allowPrivilegeEscalation: false 23 | # capabilities: 24 | # drop: 25 | # - ALL 26 | 27 | podSecurityContext: 28 | {} 29 | # fsGroup: 2000 30 | 31 | resources: 32 | limits: 33 | cpu: 500m 34 | memory: 512Mi 35 | requests: 36 | cpu: 100m 37 | memory: 128Mi 38 | 39 | resourcesInitContainer: 40 | limits: 41 | cpu: 100m 42 | memory: 128Mi 43 | requests: 44 | cpu: 50m 45 | memory: 64Mi 46 | 47 | volumes: [] 48 | 49 | volumeMounts: [] 50 | 51 | # the port the RT-FCGI server listens on 52 | port: 9000 53 | 54 | caddy: 55 | image: 56 | repository: caddy 57 | pullPolicy: Always 58 | tag: "latest" 59 | 60 | securityContext: 61 | readOnlyRootFilesystem: true 62 | runAsNonRoot: true 63 | runAsUser: 65534 # nobody user in the caddy image 64 | runAsGroup: 65534 # nobody group in the caddy image 65 | # capabilities: 66 | # drop: 67 | # - ALL 68 | 69 | resources: 70 | limits: 71 | cpu: 100m 72 | memory: 128Mi 73 | requests: 74 | cpu: 100m 75 | memory: 128Mi 76 | 77 | livenessProbe: 78 | httpGet: 79 | path: / 80 | port: health 81 | initialDelaySeconds: 10 82 | periodSeconds: 10 83 | timeoutSeconds: 10 84 | failureThreshold: 3 85 | readinessProbe: 86 | httpGet: 87 | path: / 88 | port: health 89 | initialDelaySeconds: 5 90 | periodSeconds: 5 91 | timeoutSeconds: 10 92 | failureThreshold: 3 93 | 94 | volumes: [] 95 | 96 | volumeMounts: [] 97 | 98 | ports: 99 | http: 80 100 | mailgate: 8080 101 | health: 1337 102 | 103 | service: 104 | type: ClusterIP 105 | port: 80 106 | mailgatePort: 8080 107 | 108 | postgres: 109 | enabled: true 110 | replicaCount: 1 111 | autoscaling: 112 | enabled: false 113 | minReplicas: 1 114 | maxReplicas: 10 115 | targetCPUUtilizationPercentage: 80 116 | targetMemoryUtilizationPercentage: 80 117 | image: 118 | repository: postgres 119 | pullPolicy: Always 120 | tag: "17.6" 121 | imagePullSecrets: [] 122 | 123 | securityContext: 124 | allowPrivilegeEscalation: false 125 | readOnlyRootFilesystem: true 126 | runAsNonRoot: true 127 | runAsUser: 999 # postgres user in the postgres image 128 | runAsGroup: 999 # postgres group in the postgres image 129 | # capabilities: 130 | # drop: 131 | # - ALL 132 | 133 | podSecurityContext: 134 | fsGroup: 999 # postgres group in the postgres image 135 | 136 | resources: 137 | limits: 138 | cpu: 500m 139 | memory: 512Mi 140 | requests: 141 | cpu: 100m 142 | memory: 128Mi 143 | 144 | volumes: [] 145 | 146 | volumeMounts: [] 147 | 148 | service: 149 | type: ClusterIP 150 | port: 5432 151 | 152 | serviceAccount: 153 | create: true 154 | automount: true 155 | annotations: {} 156 | 157 | podAnnotations: {} 158 | podLabels: {} 159 | 160 | ingress: 161 | enabled: true 162 | className: "nginx" 163 | annotations: 164 | kubernetes.io/ingress.class: nginx 165 | hosts: 166 | - host: chart-example.local 167 | paths: 168 | - path: / 169 | pathType: Prefix 170 | tls: [] 171 | # - secretName: chart-example-tls 172 | # hosts: 173 | # - chart-example.local 174 | 175 | # Use an openshift route to expose the service. 176 | route: 177 | enabled: false 178 | hostName: chart-example.local 179 | annotations: {} 180 | 181 | nodeSelector: {} 182 | 183 | tolerations: [] 184 | 185 | affinity: {} 186 | 187 | cronjobs: 188 | # - name: "clean-sessions" 189 | # schedule: "0 0 * * *" 190 | # restartPolicy: Never 191 | # timeZone: "UTC" 192 | # concurrencyPolicy: "Forbid" 193 | # command: 194 | # - /opt/rt/sbin/rt-clean-sessions 195 | # image: 196 | # {} # use the same image as the main pod 197 | # # repository: firefart/requesttracker 198 | # # pullPolicy: Always 199 | # # tag: "latest" 200 | # securityContext: 201 | # readOnlyRootFilesystem: true 202 | # runAsNonRoot: true 203 | # runAsUser: 1000 # the user id of the rt user in the container 204 | # runAsGroup: 1000 # the group id of the rt user in the container 205 | # allowPrivilegeEscalation: false 206 | # resources: 207 | # limits: 208 | # cpu: 100m 209 | # memory: 128Mi 210 | # requests: 211 | # cpu: 100m 212 | # memory: 128Mi 213 | 214 | # - name: "refresh-fulltext-index" 215 | # schedule: "0 * * * *" 216 | # restartPolicy: Never 217 | # timeZone: "UTC" 218 | # concurrencyPolicy: "Forbid" 219 | # command: 220 | # - /opt/rt/sbin/rt-fulltext-indexer 221 | # - --quiet 222 | # image: 223 | # {} # use the same image as the main pod 224 | # # repository: firefart/requesttracker 225 | # # pullPolicy: Always 226 | # # tag: "latest" 227 | # securityContext: 228 | # readOnlyRootFilesystem: true 229 | # runAsNonRoot: true 230 | # runAsUser: 1000 # the user id of the rt user in the container 231 | # runAsGroup: 1000 # the group id of the rt user in the container 232 | # allowPrivilegeEscalation: false 233 | # resources: 234 | # limits: 235 | # cpu: 100m 236 | # memory: 128Mi 237 | # requests: 238 | # cpu: 100m 239 | # memory: 128Mi 240 | 241 | # - name: "getmail" 242 | # schedule: "* * * * *" 243 | # restartPolicy: Never 244 | # timeZone: "UTC" 245 | # concurrencyPolicy: "Forbid" 246 | # command: 247 | # - /usr/bin/getmail 248 | # - --rcfile=/getmailrc 249 | # - -g 250 | # - /getmail 251 | # image: 252 | # {} # use the same image as the main pod 253 | # # repository: firefart/requesttracker 254 | # # pullPolicy: Always 255 | # # tag: "latest" 256 | # securityContext: 257 | # readOnlyRootFilesystem: true 258 | # runAsNonRoot: true 259 | # runAsUser: 1000 # the user id of the rt user in the container 260 | # runAsGroup: 1000 # the group id of the rt user in the container 261 | # allowPrivilegeEscalation: false 262 | # resources: 263 | # limits: 264 | # cpu: 100m 265 | # memory: 128Mi 266 | # requests: 267 | # cpu: 100m 268 | # memory: 128Mi 269 | 270 | # - name: "email-dashboards" 271 | # schedule: "0 * * * *" 272 | # restartPolicy: Never 273 | # timeZone: "UTC" 274 | # concurrencyPolicy: "Forbid" 275 | # command: 276 | # - /opt/rt/sbin/rt-email-dashboards 277 | # image: 278 | # {} # use the same image as the main pod 279 | # # repository: firefart/requesttracker 280 | # # pullPolicy: Always 281 | # # tag: "latest" 282 | # securityContext: 283 | # readOnlyRootFilesystem: true 284 | # runAsNonRoot: true 285 | # runAsUser: 1000 # the user id of the rt user in the container 286 | # runAsGroup: 1000 # the group id of the rt user in the container 287 | # allowPrivilegeEscalation: false 288 | # resources: 289 | # limits: 290 | # cpu: 100m 291 | # memory: 128Mi 292 | # requests: 293 | # cpu: 100m 294 | # memory: 128Mi 295 | 296 | # used to store shredder backups 297 | pvc: 298 | gpg: 299 | enabled: true 300 | # accessMode: ReadWriteMany 301 | accessMode: ReadWriteOnce 302 | storageClass: "" 303 | size: 1Gi 304 | smime: 305 | enabled: true 306 | # accessMode: ReadWriteMany 307 | accessMode: ReadWriteOnce 308 | storageClass: "" 309 | size: 1Gi 310 | shredder: 311 | enabled: true 312 | # accessMode: ReadWriteMany 313 | accessMode: ReadWriteOnce 314 | storageClass: "" 315 | size: 1Gi 316 | cron: 317 | enabled: true 318 | # accessMode: ReadWriteMany 319 | accessMode: ReadWriteOnce 320 | storageClass: "" 321 | size: 1Gi 322 | caddyData: 323 | enabled: true 324 | # accessMode: ReadWriteMany 325 | accessMode: ReadWriteOnce 326 | storageClass: "" 327 | size: 1Gi 328 | caddyConfig: 329 | enabled: true 330 | # accessMode: ReadWriteMany 331 | accessMode: ReadWriteOnce 332 | storageClass: "" 333 | size: 1Gi 334 | postgresData: 335 | enabled: true 336 | # accessMode: ReadWriteMany 337 | accessMode: ReadWriteOnce 338 | storageClass: "" 339 | size: 1Gi 340 | 341 | config: 342 | rtSiteConfig: | 343 | ### Base configuration ### 344 | Set($rtname, 'rt'); 345 | Set($WebDomain, 'localhost'); 346 | Set($WebPort, 443); 347 | Set($CanonicalizeRedirectURLs, 1); 348 | Set($CanonicalizeURLsInFeeds, 1); 349 | 350 | Plugin('RT::Extension::MergeUsers'); 351 | Plugin('RT::Extension::TerminalTheme'); 352 | 353 | ### Database connection ### 354 | Set($DatabaseType, 'Pg' ); 355 | Set($DatabaseHost, 'rt-db'); 356 | Set($DatabasePort, '5432'); 357 | Set($DatabaseUser, '{{ .Env.RT_DB_USER }}'); 358 | Set($DatabasePassword, '{{ .Env.RT_DB_PASS }}'); 359 | Set($DatabaseName, '{{ .Env.RT_DB_NAME }}'); 360 | Set($DatabaseAdmin, '{{ .Env.RT_DB_USER }}'); 361 | 362 | Set($SendmailPath, '/usr/bin/msmtp'); 363 | 364 | ### GnuPG configuration ### 365 | Set(%GnuPG, 366 | Enable => 1, 367 | GnuPG => 'gpg', 368 | Passphrase => undef, 369 | OutgoingMessagesFormat => 'RFC' 370 | ); 371 | 372 | Set(%GnuPGOptions, 373 | homedir => '/opt/rt/var/data/gpg', 374 | passphrase => 'PASSPHRASE', 375 | keyserver => 'hkps://keys.openpgp.org', 376 | 'keyserver-options' => 'auto-key-retrieve timeout=20', 377 | 'auto-key-locate' => 'keyserver', 378 | ); 379 | 380 | ### SMIME configuration ### 381 | Set(%SMIME, 382 | Enable => 1, 383 | AcceptUntrustedCAs => 1, 384 | OpenSSL => '/usr/bin/openssl', 385 | Keyring => '/opt/rt/var/data/smime', 386 | CAPath => '/opt/rt/var/data/smime/signing-ca.pem', 387 | Passphrase => { 388 | 'user@user.com' => 'PASSPHRASE', 389 | '' => 'fallback', 390 | }, 391 | ); 392 | 393 | 1; 394 | msmtp: | 395 | defaults 396 | 397 | account smtp 398 | host smtp.office365.com 399 | port 587 400 | tls on 401 | tls_starttls on 402 | from test@test.com 403 | tls_trust_file /msmtp/ca-bundle.crt 404 | tls_certcheck on 405 | auth on 406 | user user@domain.com 407 | password pass 408 | logfile - 409 | 410 | account default : smtp 411 | getmailrc: | 412 | [retriever] 413 | type = SimpleIMAPSSLRetriever 414 | server = mail.host.com 415 | username = user@domain.com 416 | password = pass 417 | mailboxes = ("INBOX",) 418 | 419 | [destination] 420 | type = MDA_external 421 | path = /opt/rt/bin/rt-mailgate 422 | user = rt 423 | group = rt 424 | # 8080 is the mailgate vhost 425 | arguments = ("--url", "http://rt-caddy:8080/", "--queue", "general", "--action", "correspond",) 426 | 427 | [options] 428 | read_all = false 429 | delete = true 430 | verbose = 0 431 | caddyfile: | 432 | { 433 | debug 434 | admin off 435 | auto_https off 436 | } 437 | 438 | # healthchecks 439 | :1337 { 440 | respond "OK" 200 441 | } 442 | 443 | # mailgate 444 | :8080 { 445 | log 446 | reverse_proxy localhost:9000 { 447 | transport fastcgi 448 | } 449 | } 450 | 451 | # request tracker 452 | :80 { 453 | log 454 | 455 | # Block access to the unauth mail gateway endpoint 456 | # we have a seperate mailgate server for that 457 | @blocked path /REST/1.0/NoAuth/mail-gateway 458 | respond @blocked "Nope" 403 459 | 460 | reverse_proxy localhost:9000 { 461 | transport fastcgi 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Request Tracker with Docker 2 | 3 | This is a complete setup for [Request Tracker](https://bestpractical.com/request-tracker) with docker and docker compose. The production setup assumes you have an external postgres database and an external SMTP server for outgoing emails. A local database server is only started in the dev configuration. 4 | 5 | The prebuilt image is available from [https://hub.docker.com/r/firefart/requesttracker](https://hub.docker.com/r/firefart/requesttracker). The image is rebuilt on a daily basis. 6 | 7 | The [Request Tracker for Incident Response (RT-IR)](https://bestpractical.com/rtir) extension is also installed among various others. Look at the [Dockerfile](Dockerfile) to see the available extensions. 8 | 9 | ## docker based installation 10 | 11 | ### Prerequisites 12 | 13 | - [Docker](https://docs.docker.com/get-docker/) with the `compose` plugin 14 | - an external SMTP server to send emails 15 | - an external IMAP server to receive emails from 16 | - an external Postgres database 17 | 18 | ### Instruction 19 | 20 | To start use either `./dev.sh` which builds the images locally or `./prod.sh` which uses the prebuilt ones from docker hub. Before running this you also need to add the required configuration files (see Configuration). 21 | 22 | ### Configuration 23 | 24 | The following configuration files need to be present before starting: 25 | 26 | - `RT_SiteConfig.pm` : RTs main configuration file. This needs to be present in the root of the dir. See `RT_SiteConfig.pm.example` for an example configration and the needed paths and settings for this configuration. For a full config reference have a look at the [official documentation](https://docs.bestpractical.com/rt/latest/RT_Config.html). 27 | - `Caddyfile`: The webserver config. See `Caddyfile.example` for an example and the [official Caddy doc](https://caddyserver.com/docs/caddyfile) for a reference. 28 | - `./msmtp/msmtp.conf` : config for msmtp (outgoing email). See `msmtp.conf.example` for an example. The `./msmtp` folder is also mounted to `/msmtp/` in the container so you can load certificates from the config file. [MSMTP Configuration Guide](https://marlam.de/msmtp/msmtp.html) 29 | - `crontab` : Crontab file that will be run as the RT user. See contab.example for an example. Crontab output will be sent to the MAILTO address (it uses the msmtp config). You can use [crontab guru](https://crontab.guru/) for help with the format. 30 | - `./getmail/getmailrc`: This file configures your E-Mail fetching. See `getmailrc.example` for an example. `getmail` configuration docs are available under [https://getmail6.org/configuration.html](https://getmail6.org/configuration.html). The configuration options for `rt-mailgate` which is used to store the emails in request tracker can be viewed under [https://docs.bestpractical.com/rt/latest/rt-mailgate.html](https://docs.bestpractical.com/rt/latest/rt-mailgate.html). 31 | 32 | Additional configs: 33 | 34 | - `./certs/`: This folder should contain all optional certificates needed for caddy 35 | - `./gpg/` : This folder should contain the gpg keyring if used in rt. Be sure to chmod the files to user 1000 with 0600 so RT will not complain. You can also put a `dirmngr.conf` here to configure dirmngr. For available options see [here](https://www.gnupg.org/documentation/manuals/gnupg/Dirmngr-Configuration.html) and [here](https://www.gnupg.org/documentation/manuals/gnupg/Dirmngr-Options.html). 36 | - `./smime/` : This folder should contain the SMIME certificate if configured in RT 37 | - `./shredder/` : This directory will be used by the shredder functionality [https://docs.bestpractical.com/rt/latest/RT/Shredder.html](https://docs.bestpractical.com/rt/latest/RT/Shredder.html) so the backups are stored on the host 38 | 39 | For output of your crontabs you can use the `/cron` directory so the output will be available on the host. 40 | 41 | In the default configuration all output from RT, caddy, getmail and msmtp is available via `docker logs` (or `docker compose -f ... logs`). 42 | 43 | ### Webserver 44 | 45 | The setup uses Caddy as a webserver. You can find an example configuration in [Caddyfile.example](Caddyfile.example). Caddy provides features like auto https with lets encrypt and more stuff that makes it easy to set up. You can find the Caddy documentation here [https://caddyserver.com/docs/caddyfile](https://caddyserver.com/docs/caddyfile). 46 | 47 | Feel free to modify the config to your needs like auto https, certificate based authentication, basic authentication and so on. Just be sure the mailgateway host under port `:8080` is untouched and the main host contains a block for the unauth API path, otherwise everyone with access to your RT instance can create emails without the need to log in first. 48 | 49 | #### Create Certificate 50 | 51 | If you don't want to use the auto https feature (for example in dev) you can provide your own certificates. 52 | 53 | Create a self signed certificate: 54 | 55 | ```bash 56 | openssl req -x509 -newkey rsa:4096 -keyout ./certs/priv.pem -out ./certs/pub.pem -days 3650 -nodes 57 | ``` 58 | 59 | #### Example Caddy Configurations 60 | 61 |
62 | Caddy on a domain with lets encrypt certificates 63 | 64 | ``` 65 | { 66 | admin off 67 | } 68 | 69 | # healthchecks 70 | :1337 { 71 | respond "OK" 200 72 | } 73 | 74 | # mailgate 75 | :8080 { 76 | log 77 | reverse_proxy rt:9000 { 78 | transport fastcgi 79 | } 80 | } 81 | 82 | # request tracker 83 | rt.domain.com:443 { 84 | log 85 | tls user@email.com 86 | 87 | # Block access to the unauth mail gateway endpoint 88 | # we have a seperate mailgate server for that 89 | @blocked path /REST/1.0/NoAuth/mail-gateway 90 | respond @blocked "Nope" 403 91 | 92 | reverse_proxy rt:9000 { 93 | transport fastcgi 94 | } 95 | } 96 | ``` 97 | 98 |
99 | 100 |
101 | Caddy behind a reverse proxy server with a self signed certificate 102 | 103 | `pub.pem` and `priv.pem` need to be inside the `./certs` folder and will be mounted automatically. 104 | 105 | ``` 106 | { 107 | admin off 108 | auto_https off 109 | 110 | servers { 111 | trusted_proxies static 10.0.0.0/22 112 | client_ip_headers X-Orig-Addr 113 | trusted_proxies_strict 114 | } 115 | } 116 | 117 | # healthchecks 118 | :1337 { 119 | respond "OK" 200 120 | } 121 | 122 | # mailgate 123 | :8080 { 124 | log 125 | reverse_proxy rt:9000 { 126 | transport fastcgi 127 | } 128 | } 129 | 130 | # request tracker 131 | :443 { 132 | log 133 | 134 | tls /certs/pub.pem /certs/priv.pem 135 | 136 | # Block access to the unauth mail gateway endpoint 137 | # we have a seperate mailgate server for that 138 | @blocked path /REST/1.0/NoAuth/mail-gateway 139 | respond @blocked "Nope" 403 140 | 141 | reverse_proxy rt:9000 { 142 | transport fastcgi { 143 | env SERVER_NAME {http.request.header.X-Orig-HostHeader} 144 | } 145 | } 146 | } 147 | ``` 148 | 149 |
150 | 151 |
152 | Caddy behind a reverse proxy server with a self signed certificate and client certificate validation 153 | 154 | `pub.pem`, `priv.pem` and `root-ca.pem` need to be inside the `./certs` folder and will be mounted automatically. 155 | 156 | ``` 157 | { 158 | admin off 159 | auto_https off 160 | 161 | servers { 162 | trusted_proxies static 10.0.0.0/22 163 | client_ip_headers X-Orig-Addr 164 | trusted_proxies_strict 165 | } 166 | } 167 | 168 | # healthchecks 169 | :1337 { 170 | respond "OK" 200 171 | } 172 | 173 | # mailgate 174 | :8080 { 175 | log 176 | reverse_proxy rt:9000 { 177 | transport fastcgi 178 | } 179 | } 180 | 181 | # request tracker 182 | :443 { 183 | log 184 | 185 | tls /certs/pub.pem /certs/priv.pem { 186 | protocols tls1.3 187 | client_auth { 188 | mode require_and_verify 189 | trust_pool file /certs/root-ca.pem 190 | } 191 | } 192 | 193 | # Block access to the unauth mail gateway endpoint 194 | # we have a seperate mailgate server for that 195 | @blocked path /REST/1.0/NoAuth/mail-gateway 196 | respond @blocked "Nope" 403 197 | 198 | reverse_proxy rt:9000 { 199 | transport fastcgi { 200 | env SERVER_NAME {http.request.header.X-Orig-HostHeader} 201 | } 202 | } 203 | } 204 | ``` 205 | 206 |
207 | 208 |
209 | Caddy behind a reverse proxy server with a self signed certificate and client certificate validation with subject validation 210 | 211 | `pub.pem`, `priv.pem` and `root-ca.pem` need to be inside the `./certs` folder and will be mounted automatically. 212 | 213 | ``` 214 | { 215 | admin off 216 | auto_https off 217 | 218 | servers { 219 | trusted_proxies static 10.0.0.0/22 220 | client_ip_headers X-Orig-Addr 221 | trusted_proxies_strict 222 | } 223 | } 224 | 225 | # healthchecks 226 | :1337 { 227 | respond "OK" 200 228 | } 229 | 230 | # mailgate 231 | :8080 { 232 | log 233 | reverse_proxy rt:9000 { 234 | transport fastcgi 235 | } 236 | } 237 | 238 | # request tracker 239 | :443 { 240 | @cert-auth { 241 | expression {http.request.tls.client.subject} == "CN=Subject,OU=example,O=com,C=xxx" 242 | } 243 | 244 | log 245 | 246 | tls /certs/pub.pem /certs/priv.pem { 247 | protocols tls1.3 248 | client_auth { 249 | mode require_and_verify 250 | trust_pool file /certs/root-ca.pem 251 | } 252 | } 253 | 254 | # block everything that is not from a trusted ip range 255 | @blocked_trusted not remote_ip 10.0.0.0/22 256 | respond @blocked_trusted "Nope" 403 257 | 258 | # Block access to the unauth mail gateway endpoint 259 | # we have a seperate mailgate server for that 260 | @blocked path /REST/1.0/NoAuth/mail-gateway 261 | respond @blocked "Nope" 403 262 | 263 | reverse_proxy @cert-auth rt:9000 { 264 | transport fastcgi { 265 | env SERVER_NAME {http.request.header.X-Orig-HostHeader} 266 | } 267 | } 268 | } 269 | ``` 270 | 271 |
272 | 273 |
274 | Caddy behind a reverse proxy server with a self signed certificate and client certificate validation with subject validation and on a subpath 275 | 276 | `pub.pem`, `priv.pem` and `root-ca.pem` need to be inside the `./certs` folder and will be mounted automatically. The reverse proxy needs to point to `servername/rt` otherwise you will end up with wrong paths in the cookies which will lead to file uploads not working correctly. 277 | We will also set the REMOTE_USER to a custom header sent from the upstream proxy. 278 | 279 | ``` 280 | { 281 | admin off 282 | auto_https off 283 | 284 | servers { 285 | trusted_proxies static 10.0.0.0/22 286 | client_ip_headers X-Orig-Addr 287 | trusted_proxies_strict 288 | } 289 | } 290 | 291 | # healthchecks 292 | :1337 { 293 | respond "OK" 200 294 | } 295 | 296 | # mailgate 297 | :8080 { 298 | log 299 | reverse_proxy rt:9000 { 300 | transport fastcgi 301 | } 302 | } 303 | 304 | # request tracker 305 | :443 { 306 | @cert-auth { 307 | expression {http.request.tls.client.subject} == "CN=Subject,OU=example,O=com,C=xxx" 308 | } 309 | 310 | log 311 | tls /certs/pub.pem /certs/priv.pem { 312 | protocols tls1.3 313 | client_auth { 314 | mode require_and_verify 315 | trust_pool file /certs/root-ca.pem 316 | } 317 | } 318 | 319 | # block everything that is not from a trusted ip range 320 | @blocked_trusted not remote_ip 10.0.0.0/22 321 | respond @blocked_trusted "Nope" 403 322 | 323 | handle_path /rt/* { 324 | # Block access to the unauth mail gateway endpoint 325 | # we have a seperate mailgate server for that 326 | @blocked path /REST/1.0/NoAuth/mail-gateway 327 | respond @blocked "Nope" 403 328 | 329 | reverse_proxy @cert-auth rt:9000 { 330 | transport fastcgi { 331 | env REMOTE_USER {http.request.header.X-Auth-Username} 332 | env SERVER_NAME {http.request.header.X-Orig-HostHeader} 333 | env REQUEST_URI {uri} 334 | } 335 | } 336 | } 337 | } 338 | ``` 339 | 340 |
341 | 342 | ### Init database 343 | 344 | This initializes a fresh database. This is needed on the first run. 345 | 346 | ```bash 347 | docker compose run --rm rt bash -c 'cd /opt/rt && perl ./sbin/rt-setup-database --action init' 348 | ``` 349 | 350 | You need to restart the rt service after this step as it crashes if the database is not initialized. 351 | 352 | #### DEV 353 | 354 | Hint: Add `--skip-create` in dev as the database is created by docker 355 | 356 | ```bash 357 | docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm rt bash -c 'cd /opt/rt && perl ./sbin/rt-setup-database --action init --skip-create' 358 | ``` 359 | 360 | ### Upgrade steps 361 | 362 | #### Upgrade Database 363 | 364 | ```bash 365 | docker compose run --rm rt bash -c 'cd /opt/rt && perl ./sbin/rt-setup-database --action upgrade --upgrade-from 4.4.4' 366 | ``` 367 | 368 | #### Fix data inconsistencies 369 | 370 | Run multiple times with the `--resolve` switch until no errors occur 371 | 372 | ```bash 373 | docker compose run --rm rt bash -c 'cd /opt/rt && perl ./sbin/rt-validator --check --resolve' 374 | ``` 375 | 376 | ### RT-IR 377 | 378 | You can simply enable RT-IR in your `RT_SiteConfig.pm` by including `Plugin('RT::IR');`. Please refer to the [docs](https://docs.bestpractical.com/rtir/latest/index.html) for additional install or upgrade steps. 379 | 380 | To initialize the database (ONLY ON THE FIRST RUN!!!! and only after rt is fully set up) 381 | 382 | ```bash 383 | docker compose run --rm rt bash -c 'cd /opt/rt && perl ./sbin/rt-setup-database --action insert --skip-create --datafile /opt/rtir/initialdata' 384 | ``` 385 | 386 | To upgrade 387 | 388 | ```bash 389 | docker compose run --rm rt bash -c 'cd /opt/rt && perl ./sbin/rt-setup-database --action upgrade --skip-create --datadir /opt/rtir/upgrade --package RT::IR --ext-version 5.0.4' 390 | ``` 391 | 392 | Restart docker setup after all steps to fully load RT-IR (just run `./restart_prod.sh`). 393 | 394 | ### Extending 395 | 396 | To include additional containers in this setup like pgadmin or change a default config, you can create a `docker-compose.override.yml` file in the projects root and it will automatically picked up and merged with the default config. Run `docker compose config` to view the merged config. 397 | 398 | ### Deprecated features 399 | 400 | - NGINX: The old setup used nginx for the webserver. If you want to upgrade you need to migrate your nginx config to a Caddy config. See the example Caddy Configuration section for some ideas. 401 | - compose profiles: Previously there were compose profile to also include `dozzle` for viewing logs and `pgadmin` to interact with the database. Both tools are now removed and `pgadmin` is only available in dev mode. If you still need pgadmin you can easily spin it up using docker compose. 402 | 403 | ## Kubernetes setup 404 | 405 | ```bash 406 | helm install rt helm/ 407 | ``` 408 | 409 | ### Setup database 410 | 411 | ```bash 412 | kubectl apply -f k8s-jobs/db-init.yaml 413 | ``` 414 | 415 | ### Upgrade database 416 | 417 | ```bash 418 | kubectl apply -f k8s-jobs/db-update.yaml 419 | ``` 420 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM debian:13-slim AS msmtp-builder 4 | 5 | ENV MSMTP_VERSION="1.8.32" 6 | ENV MSMTP_GPG_PUBLIC_KEY="2F61B4828BBA779AECB3F32703A2A4AB1E32FD34" 7 | 8 | # Install required packages 9 | RUN DEBIAN_FRONTEND=noninteractive apt-get update \ 10 | && apt-get -q -y install --no-install-recommends \ 11 | wget ca-certificates libgnutls28-dev xz-utils \ 12 | gpg dirmngr gpg-agent libgsasl-dev libsecret-1-dev \ 13 | build-essential automake libtool gettext texinfo pkg-config 14 | 15 | RUN wget -O /msmtp.tar.xz -nv https://marlam.de/msmtp/releases/msmtp-${MSMTP_VERSION}.tar.xz \ 16 | && wget -O /msmtp.tar.xz.sig -nv https://marlam.de/msmtp/releases/msmtp-${MSMTP_VERSION}.tar.xz.sig \ 17 | && gpg --keyserver hkps://keyserver.ubuntu.com --keyserver-options timeout=10 --recv-keys ${MSMTP_GPG_PUBLIC_KEY} \ 18 | && gpg --verify /msmtp.tar.xz.sig /msmtp.tar.xz \ 19 | && tar -xf /msmtp.tar.xz \ 20 | && cd /msmtp-${MSMTP_VERSION} \ 21 | && ./configure --sysconfdir=/etc \ 22 | && make \ 23 | && make install 24 | 25 | ############################################################################# 26 | 27 | FROM perl:5.42.0 AS builder 28 | 29 | ARG RT_VERSION="6.0.2" 30 | ARG RTIR_VERSION="6.0.1" 31 | 32 | ENV RT="${RT_VERSION}" 33 | ENV RTIR="${RTIR_VERSION}" 34 | ENV RT_GPG_PUBLIC_KEY="C49B372F2BF84A19011660270DF0A283FEAC80B2" 35 | 36 | ARG ADDITIONAL_CPANM_ARGS="" 37 | 38 | # use cpanm for dependencies 39 | ENV RT_FIX_DEPS_CMD="cpanm -v --no-man-pages ${ADDITIONAL_CPANM_ARGS}" 40 | # cpan non interactive mode 41 | ENV PERL_MM_USE_DEFAULT=1 42 | 43 | # Create RT user 44 | RUN groupadd -g 1000 rt && useradd -u 1000 -g 1000 -m -s /bin/bash -d /home/rt rt 45 | 46 | # Install required packages 47 | RUN DEBIAN_FRONTEND=noninteractive apt-get update \ 48 | && apt-get -q -y install --no-install-recommends \ 49 | ca-certificates wget gnupg graphviz libssl3 zlib1g \ 50 | gpg dirmngr gpg-agent \ 51 | libgd3 libexpat1 libpq5 w3m elinks links html2text lynx openssl libgd-dev 52 | 53 | # Download and extract RT 54 | RUN mkdir -p /src \ 55 | # import RT signing key 56 | && gpg --keyserver hkps://keyserver.ubuntu.com --keyserver-options timeout=10 --recv-keys ${RT_GPG_PUBLIC_KEY} \ 57 | # download and extract RT 58 | && wget -O /src/rt.tar.gz -nv https://download.bestpractical.com/pub/rt/release/rt-${RT}.tar.gz \ 59 | && wget -O /src/rt.tar.gz.asc -nv https://download.bestpractical.com/pub/rt/release/rt-${RT}.tar.gz.asc \ 60 | && gpg --verify /src/rt.tar.gz.asc /src/rt.tar.gz \ 61 | && mkdir -p /src/rt \ 62 | && tar --strip-components=1 -C /src/rt -xzf /src/rt.tar.gz \ 63 | # download and extract RTIR 64 | && wget -O /src/rtir.tar.gz -nv https://download.bestpractical.com/pub/rt/release/RT-IR-${RTIR}.tar.gz \ 65 | && wget -O /src/rtir.tar.gz.asc -nv https://download.bestpractical.com/pub/rt/release/RT-IR-${RTIR}.tar.gz.asc \ 66 | && gpg --verify /src/rtir.tar.gz.asc /src/rtir.tar.gz \ 67 | && mkdir -p /src/rtir \ 68 | && tar --strip-components=1 -C /src/rtir -xzf /src/rtir.tar.gz 69 | 70 | # Configure RT 71 | RUN case "${RT_VERSION}" in \ 72 | "6."*) \ 73 | cd /src/rt \ 74 | && ./configure --prefix=/opt/rt --with-db-type=Pg --enable-gpg --enable-dashboard-chart-emails --enable-graphviz --enable-smime --enable-externalauth --with-web-user=rt --with-web-group=rt --with-rt-group=rt --with-bin-owner=rt --with-libs-owner=rt \ 75 | ;; \ 76 | # older versions for RT 5.0.x 77 | "5."*) \ 78 | cd /src/rt \ 79 | && ./configure --prefix=/opt/rt --with-db-type=Pg --enable-gpg --enable-gd --enable-graphviz --enable-smime --enable-externalauth --with-web-user=rt --with-web-group=rt --with-rt-group=rt --with-bin-owner=rt --with-libs-owner=rt \ 80 | ;; \ 81 | esac 82 | 83 | # install https support for cpanm 84 | # also disable tests on net http as the live tests often fail 85 | RUN cpanm -v --no-man-pages -n install Net::HTTP \ 86 | && cpanm -v --no-man-pages install LWP::Protocol::https \ 87 | # Install Sever::Starter without tests 88 | # as they constanly fail with timeouts and thus break 89 | # the build 90 | # Also install CSS::Inliner so users can use $EmailDashboardInlineCSS 91 | && cpanm -v --no-man-pages -n install Server::Starter CSS::Inliner \ 92 | # https://github.com/Corion/WWW-Mechanize-Chrome/issues/85 93 | && cpanm -v --no-man-pages -n install Filter::signatures \ 94 | # tests fail on build 95 | && cpanm -v --no-man-pages -n install Cache::Cache \ 96 | # tests fail on build 97 | && cpanm -v --no-man-pages -n install Time::ParseDate 98 | 99 | # Install dependencies 100 | RUN make -C /src/rt fixdeps \ 101 | && make -C /src/rt testdeps \ 102 | && make -C /src/rt install 103 | 104 | ENV PERL5LIB=/opt/rt/lib/ 105 | 106 | # install extensions and additional tools 107 | RUN true \ 108 | # https://metacpan.org/dist/RT-Extension-MergeUsers 109 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::MergeUsers \ 110 | # https://metacpan.org/dist/RT-Extension-TerminalTheme 111 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::TerminalTheme \ 112 | # https://metacpan.org/dist/RT-Extension-Announce 113 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::Announce \ 114 | # https://metacpan.org/dist/RT-Extension-Assets-Import-CSV 115 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::Assets::Import::CSV \ 116 | # https://metacpan.org/dist/RT-Extension-Import-CSV 117 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::Import::CSV \ 118 | # https://metacpan.org/dist/RT-Extension-CommandByMail 119 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::CommandByMail \ 120 | # https://metacpan.org/dist/RT-Extension-ExtractCustomFieldValues 121 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ExtractCustomFieldValues \ 122 | # https://metacpan.org/dist/RT-Extension-JSGantt 123 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::JSGantt \ 124 | # https://metacpan.org/dist/RT-Extension-NonWatcherRecipients 125 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::NonWatcherRecipients \ 126 | # https://metacpan.org/dist/RTx-TicketlistTransactions 127 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RTx::TicketlistTransactions \ 128 | # https://metacpan.org/dist/RTx-RemoteLinks 129 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RTx::RemoteLinks \ 130 | # https://metacpan.org/dist/RT-Extension-TicketLocking 131 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::TicketLocking \ 132 | # https://metacpan.org/dist/RT-Extension-DynamicWebPath 133 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::DynamicWebPath \ 134 | # https://metacpan.org/dist/RT-Authen-OAuth2 135 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Authen::OAuth2 \ 136 | # https://metacpan.org/dist/RT-Extension-RepliesToResolved 137 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::RepliesToResolved \ 138 | # https://metacpan.org/dist/RT-Extension-ShowTransactionSquelching 139 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ShowTransactionSquelching \ 140 | # https://github.com/bestpractical/app-wsgetmail 141 | # https://metacpan.org/dist/App-wsgetmail 142 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} App::wsgetmail 143 | 144 | # extensions for RT 6.0.x 145 | RUN case "${RT_VERSION}" in \ 146 | "6."*) \ 147 | # https://metacpan.org/dist/RT-Extension-MandatoryOnTransition 148 | cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::MandatoryOnTransition \ 149 | # https://metacpan.org/dist/RT-Extension-ExcelFeed 150 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ExcelFeed \ 151 | # https://metacpan.org/dist/RT-Extension-AutomaticAssignment 152 | # no tests here as it would require a database 153 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} -n RT::Extension::AutomaticAssignment \ 154 | # https://metacpan.org/dist/RT-Extension-FormTools 155 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::FormTools \ 156 | # https://metacpan.org/dist/RT-Extension-RepeatTicket 157 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::RepeatTicket \ 158 | # https://metacpan.org/dist/RTx-Calendar 159 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RTx::Calendar \ 160 | # https://metacpan.org/dist/RT-Extension-ActivityReports 161 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ActivityReports \ 162 | # https://metacpan.org/dist/RT-Extension-InlineHelp (only for RT 6.0.x) 163 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::InlineHelp \ 164 | # https://metacpan.org/dist/RT-Extension-Tags 165 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::Tags \ 166 | # https://metacpan.org/dist/RT-Extension-HelpDesk 167 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::HelpDesk \ 168 | # https://metacpan.org/dist/RT-Extension-AI (only for RT 6.0.x) 169 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::AI \ 170 | # https://metacpan.org/dist/RT-Extension-ChangeManagement 171 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ChangeManagement \ 172 | # https://metacpan.org/dist/RT-Extension-SwitchUsers 173 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::SwitchUsers \ 174 | # https://metacpan.org/dist/RT-Extension-ResetPassword 175 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ResetPassword \ 176 | # https://metacpan.org/dist/RT-Extension-Captcha 177 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::Captcha \ 178 | # https://metacpan.org/dist/RT-Extension-QuickCalls 179 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::QuickCalls \ 180 | ;; \ 181 | # older versions for RT 5.0.x 182 | "5."*) \ 183 | # https://metacpan.org/dist/RT-Extension-MandatoryOnTransition 184 | cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::MandatoryOnTransition~">= 0.0000, < 1.0000" \ 185 | # https://metacpan.org/dist/RT-Extension-ExcelFeed 186 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ExcelFeed~">= 0.0000, < 1.0000" \ 187 | # https://metacpan.org/dist/RT-Extension-AutomaticAssignment 188 | # no tests here as it would require a database 189 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} -n RT::Extension::AutomaticAssignment~">= 1.0000, < 2.0000" \ 190 | # https://metacpan.org/dist/RT-Extension-FormTools 191 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::FormTools~">= 1.0000, < 2.0000" \ 192 | # https://metacpan.org/dist/RT-Extension-RepeatTicket 193 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::RepeatTicket~">= 2.0000, < 3.0000" \ 194 | # https://metacpan.org/dist/RTx-Calendar 195 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RTx::Calendar~">= 1.0000, < 2.0000" \ 196 | # https://metacpan.org/dist/RT-Extension-ActivityReports 197 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ActivityReports~">= 1.0000, < 2.0000" \ 198 | # https://metacpan.org/dist/RT-Extension-Tags 199 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::Tags~">= 0.0000, < 1.0000" \ 200 | # https://metacpan.org/dist/RT-Extension-HelpDesk 201 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::HelpDesk~">= 0.0000, < 1.0000" \ 202 | # https://metacpan.org/dist/RT-Extension-ChangeManagement 203 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ChangeManagement~">= 0.0000, < 1.0000" \ 204 | # https://metacpan.org/dist/RT-Extension-SwitchUsers 205 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::SwitchUsers~">= 0.0000, < 1.0000" \ 206 | # https://metacpan.org/dist/RT-Extension-ResetPassword 207 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::ResetPassword~">= 1.0000, < 2.0000" \ 208 | # https://metacpan.org/dist/RT-Extension-Captcha 209 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::Captcha~">= 1.0000, < 2.0000" \ 210 | # https://metacpan.org/dist/RT-Extension-QuickCalls 211 | && cpanm -v --install --no-man-pages ${ADDITIONAL_CPANM_ARGS} RT::Extension::QuickCalls~">= 1.0000, < 2.0000" \ 212 | ;; \ 213 | esac 214 | 215 | # Configure RTIR 216 | RUN true \ 217 | && cd /src/rtir \ 218 | && perl -I /src/rtir/lib Makefile.PL --defaultdeps \ 219 | && make install 220 | 221 | # Dumb fix for HTMX Bug which rt team refuses to fix 222 | # the main page does not honor WebPath and breaks if RT is not installed 223 | # in the webserver root 224 | RUN true && \ 225 | case "${RT_VERSION}" in \ 226 | "6."*) \ 227 | sed -i 's/hx-get="<% RT::Interface::Web::RequestENV('"'"'REQUEST_URI'"'"') %>"/hx-get="<%RT->Config->Get('"'"'WebPath'"'"')%><% RT::Interface::Web::RequestENV('"'"'REQUEST_URI'"'"') %>"/' /opt/rt/share/html/Elements/Header \ 228 | ;; \ 229 | esac 230 | 231 | ############################################################################# 232 | 233 | FROM perl:5.42.0-slim 234 | LABEL org.opencontainers.image.authors="firefart " 235 | LABEL org.opencontainers.image.title="Request Tracker" 236 | LABEL org.opencontainers.image.source="https://github.com/firefart/rt-docker" 237 | LABEL org.opencontainers.image.description="Request Tracker Docker Setup" 238 | 239 | # Install required packages 240 | RUN DEBIAN_FRONTEND=noninteractive apt-get update \ 241 | && apt-get -q -y install --no-install-recommends \ 242 | procps spawn-fcgi ca-certificates wget curl gnupg graphviz libssl3 \ 243 | zlib1g libgd3 libexpat1 libpq5 w3m elinks links html2text lynx openssl cron bash \ 244 | libfcgi-bin libgsasl18 libsecret-1-0 tzdata \ 245 | && apt-get clean \ 246 | && rm -rf /var/lib/apt/lists/* 247 | # msmtp - disabled for now to use the newer version 248 | 249 | # Create RT user 250 | RUN useradd -u 1000 -s /bin/bash -d /home/rt -m rt 251 | 252 | # copy msmtp 253 | COPY --from=msmtp-builder /usr/local/bin/msmtp /usr/bin/msmtp 254 | COPY --from=msmtp-builder /usr/local/share/locale /usr/local/share/locale 255 | 256 | # copy all needed stuff from the builder image 257 | COPY --from=builder /usr/local/lib/perl5 /usr/local/lib/perl5 258 | COPY --chown=rt:rt --from=builder /opt/rt /opt/rt 259 | # run a final dependency check if we copied all 260 | RUN perl /opt/rt/sbin/rt-test-dependencies --with-pg --with-fastcgi --with-gpg --with-graphviz --with-gd 261 | 262 | # uv and uvx (needed for getmail6) 263 | COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/ 264 | 265 | RUN true \ 266 | # msmtp config 267 | && mkdir -p /msmtp \ 268 | && chown rt:rt /msmtp \ 269 | # also fake sendmail for cronjobs 270 | && ln -s /usr/bin/msmtp /usr/sbin/sendmail \ 271 | # getmail 272 | && mkdir -p /getmail \ 273 | && chown rt:rt /getmail \ 274 | # gpg 275 | && mkdir -p /opt/rt/var/data/gpg \ 276 | && chown rt:rt /opt/rt/var/data/gpg \ 277 | # smime 278 | && mkdir -p /opt/rt/var/data/smime \ 279 | && chown rt:rt /opt/rt/var/data/smime \ 280 | # shredder dir 281 | && mkdir -p /opt/rt/var/data/RT-Shredder \ 282 | && chown rt:rt /opt/rt/var/data/RT-Shredder \ 283 | # gpg dirmngr dirs 284 | && mkdir -p /home/rt/.gnupg \ 285 | && mkdir -p /home/rt/.gnupg/crls.d \ 286 | && chown -R rt:rt /home/rt/.gnupg \ 287 | && chmod 700 /home/rt/.gnupg 288 | 289 | # RTIR Database stuff for setup 290 | COPY --chown=rt:rt --from=builder /src/rtir/etc /opt/rtir 291 | 292 | # wsgetmail 293 | COPY --chown=rt:rt --from=builder /usr/local/bin/wsgetmail /usr/local/bin/wsgetmail 294 | 295 | # remove default cron jobs 296 | RUN rm -f /etc/cron.d/* \ 297 | && rm -f /etc/cron.daily/* \ 298 | && rm -f /etc/cron.hourly/* \ 299 | && rm -f /etc/cron.monthly/* \ 300 | && rm -f /etc/cron.weekly/* \ 301 | && rm -f /var/spool/cron/crontabs/* 302 | 303 | COPY --chown=root:root --chmod=0700 cron_entrypoint.sh /root/cron_entrypoint.sh 304 | 305 | EXPOSE 9000 306 | 307 | # install getmail as the rt user 308 | USER rt 309 | RUN uv tool install getmail6 310 | 311 | USER root 312 | # link getmail to /usr/bin for backwards compatibility 313 | RUN ln -s /home/rt/.local/bin/getmail /usr/bin/getmail 314 | 315 | USER rt 316 | # update PATH 317 | ENV PATH="${PATH}:/opt/rt/sbin:/opt/rt/bin:/home/rt/.local/bin" 318 | 319 | WORKDIR /opt/rt/ 320 | 321 | # spawn-fcgi v1.6.4 (ipv6) - spawns FastCGI processes 322 | 323 | # Options: 324 | # -f filename of the fcgi-application (deprecated; ignored if 325 | # is given; needs /bin/sh) 326 | # -d chdir to directory before spawning 327 | # -a
bind to IPv4/IPv6 address (defaults to 0.0.0.0) 328 | # -p bind to TCP-port 329 | # -s bind to Unix domain socket 330 | # -M change Unix domain socket mode (octal integer, default: allow 331 | # read+write for user and group as far as umask allows it) 332 | # -C (PHP only) numbers of childs to spawn (default: not setting 333 | # the PHP_FCGI_CHILDREN environment variable - PHP defaults to 0) 334 | # -F number of children to fork (default 1) 335 | # -b backlog to allow on the socket (default 1024) 336 | # -P name of PID-file for spawned process (ignored in no-fork mode) 337 | # -n no fork (for daemontools) 338 | # -v show version 339 | # -?, -h show this help 340 | # (root only) 341 | # -c chroot to directory 342 | # -S create socket before chroot() (default is to create the socket 343 | # in the chroot) 344 | # -u change to user-id 345 | # -g change to group-id (default: primary group of user if -u 346 | # is given) 347 | # -U change Unix domain socket owner to user-id 348 | # -G change Unix domain socket group to group-id 349 | CMD [ "/usr/bin/spawn-fcgi", "-d", "/opt/rt/", "-p" ,"9000", "-a","0.0.0.0", "-u", "1000", "-n", "--", "/opt/rt/sbin/rt-server.fcgi" ] 350 | 351 | HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 CMD REQUEST_METHOD=GET REQUEST_URI=/ SCRIPT_NAME=/ cgi-fcgi -connect localhost:9000 -bind || exit 1 352 | --------------------------------------------------------------------------------