diff --git a/.gitea/workflows/container.yml b/.gitea/workflows/container.yml new file mode 100644 index 0000000..f1a75c1 --- /dev/null +++ b/.gitea/workflows/container.yml @@ -0,0 +1,150 @@ +name: Publish PHP Apache image + +on: + push: + workflow_dispatch: + schedule: + # Refresh the rolling Arch Linux base weekly. + - cron: "13 3 * * 1" + +env: + IMAGE: registry.brunner.ninja/feedc0de/php-apache + +jobs: + publish: + name: Build, test and push + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Validate Helm chart + run: | + set -euo pipefail + docker run --rm \ + --volume "${PWD}:/source" \ + --workdir /source \ + alpine/helm:4.2.2@sha256:ee6fe3e96d9f8ea8dd1af9ecd7bbb3e233616a25f145392376f020fd2a51eb33 \ + lint helm/php-homepage + docker run --rm \ + --volume "${PWD}:/source" \ + --workdir /source \ + alpine/helm:4.2.2@sha256:ee6fe3e96d9f8ea8dd1af9ecd7bbb3e233616a25f145392376f020fd2a51eb33 \ + template ci-test helm/php-homepage > /dev/null + + - name: Log in to Quay + uses: docker/login-action@v3 + with: + registry: registry.brunner.ninja + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_TOKEN }} + + - name: Build image + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + docker build \ + --pull \ + --load \ + --label "org.opencontainers.image.revision=${COMMIT_SHA}" \ + --label "org.opencontainers.image.source=https://code.brunner.ninja/feedc0de/php-apache" \ + --tag "${IMAGE}:${COMMIT_SHA}" \ + . + + - name: Test Apache and PHP extensions + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + docker run --rm --entrypoint httpd "${IMAGE}:${COMMIT_SHA}" -t + docker run --rm --entrypoint php "${IMAGE}:${COMMIT_SHA}" -r ' + foreach (["exif", "gd", "intl", "Zend OPcache", "pgsql", "pdo_pgsql"] as $extension) { + if (!extension_loaded($extension)) { + fwrite(STDERR, "Missing PHP extension: {$extension}\n"); + exit(1); + } + } + $info = gd_info(); + foreach (["JPEG Support", "PNG Support", "WebP Support"] as $feature) { + if (empty($info[$feature])) { + fwrite(STDERR, "Missing GD feature: {$feature}\n"); + exit(1); + } + } + $image = imagecreatetruecolor(8, 8); + $scaled = imagescale($image, 4, 4); + if ($scaled === false || imagesx($scaled) !== 4 || imagesy($scaled) !== 4) { + fwrite(STDERR, "GD resize smoke test failed.\n"); + exit(1); + } + echo "GD resize smoke test passed.\n"; + ' + + - name: Determine version tag + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + version_tag="$( + docker run --rm --entrypoint sh "${IMAGE}:${COMMIT_SHA}" -ec ' + apache_version="$(httpd -v | sed -n "s#^Server version: Apache/\([0-9.]*\).*$#\1#p")" + php_version="$(php -r "echo PHP_VERSION;")" + test -n "${apache_version}" + test -n "${php_version}" + printf "%s-%s" "${apache_version}" "${php_version}" + ' + )" + printf 'VERSION_TAG=%s\n' "${version_tag}" >> "${GITHUB_ENV}" + run_attempt="${GITHUB_RUN_ATTEMPT:-1}" + test -n "${GITHUB_RUN_NUMBER}" + chart_version="0.${GITHUB_RUN_NUMBER}.${run_attempt}" + printf 'CHART_VERSION=%s\n' "${chart_version}" >> "${GITHUB_ENV}" + printf 'BUILD_TAG=chart-%s\n' "${chart_version}" >> "${GITHUB_ENV}" + + - name: Push commit tag + if: gitea.event_name == 'push' + env: + COMMIT_SHA: ${{ gitea.sha }} + run: docker push "${IMAGE}:${COMMIT_SHA}" + + - name: Push main tags + if: gitea.ref == 'refs/heads/main' || gitea.event_name == 'schedule' + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + docker tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:${BUILD_TAG}" + docker tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:${VERSION_TAG}" + docker tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:latest" + docker push "${IMAGE}:${BUILD_TAG}" + docker push "${IMAGE}:${VERSION_TAG}" + docker push "${IMAGE}:latest" + + - name: Publish Helm chart + if: gitea.ref == 'refs/heads/main' || gitea.event_name == 'schedule' + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + run: | + set -euo pipefail + test -n "${PACKAGE_USERNAME}" + test -n "${PACKAGE_TOKEN}" + install -d .chart-packages + docker run --rm \ + --volume "${PWD}:/source" \ + --workdir /source \ + alpine/helm:4.2.2@sha256:ee6fe3e96d9f8ea8dd1af9ecd7bbb3e233616a25f145392376f020fd2a51eb33 \ + package helm/php-homepage \ + --destination .chart-packages \ + --version "${CHART_VERSION}" \ + --app-version "${BUILD_TAG}" + chart_file=".chart-packages/php-homepage-${CHART_VERSION}.tgz" + test -f "${chart_file}" + curl --fail --silent --show-error \ + --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \ + --request POST \ + --upload-file "${chart_file}" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts diff --git a/Dockerfile b/Dockerfile index e5bbe52..f77f105 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM archlinux +FROM archlinux:latest -RUN pacman -Syu --noconfirm apache php-apache php \ +RUN pacman -Syu --noconfirm apache php-apache php php-gd php-pgsql \ && pacman -Sc --noconfirm \ && rm -v /var/log/pacman.log /var/lib/pacman/sync/* @@ -12,6 +12,15 @@ RUN sed -i '/mod_mpm_event/ s/^/#/' /etc/httpd/conf/httpd.conf \ && sed -i '/mod_rewrite/ s/^#//' /etc/httpd/conf/httpd.conf \ && echo LoadModule php_module modules/libphp.so >> /etc/httpd/conf/httpd.conf \ && echo AddHandler php-script .php >> /etc/httpd/conf/httpd.conf \ - && echo Include conf/extra/php_module.conf >> /etc/httpd/conf/httpd.conf + && echo Include conf/extra/php_module.conf >> /etc/httpd/conf/httpd.conf \ + && install -d /etc/php/conf.d \ + && sed -i -e '/extension=exif/ s/^;//' \ + -e '/extension=gd/ s/^;//' \ + -e '/extension=intl/ s/^;//' \ + -e '/extension=pgsql/ s/^;//' \ + -e '/extension=pdo_pgsql/ s/^;//' \ + /etc/php/php.ini -ENTRYPOINT httpd -DFOREGROUND +COPY opcache.ini /etc/php/conf.d/opcache.ini + +ENTRYPOINT ["httpd", "-DFOREGROUND"] diff --git a/README.md b/README.md index 40038ba..dffb22c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,52 @@ # php-apache -Simple docker image to run latest apache with latest php (provided by arch) \ No newline at end of file +Arch Linux container image with Apache and PHP. It is the common runtime for +the PHP sites hosted at brunner.ninja, including WordPress and phpPgAdmin. + +The image enables PostgreSQL and PDO PostgreSQL, GD for image thumbnails and +responsive sizes, EXIF for photo metadata, Intl for locale-aware operations, +and a production-sized PHP OPcache. Timestamp checks remain enabled so edits +made through the browser-based development container and WordPress updates on +the shared CephFS volume become visible without restarting Apache. + +## Continuous integration + +The Gitea Actions workflow builds and tests the image on every push. It also +rebuilds the rolling Arch Linux base every Monday. Push builds publish the +image to `registry.brunner.ninja/feedc0de/php-apache` with the Git commit SHA. +Builds of `main` additionally publish `latest` and an +`-` tag. Scheduled and manually dispatched builds +do not overwrite commit tags; they refresh only the mutable main tags. + +The workflow requires these repository secrets: + +- `QUAY_USERNAME`: Quay robot-account username with write access to the image. +- `QUAY_TOKEN`: token for that robot account. +- `PACKAGE_USERNAME`: Gitea user that publishes packages for `feedc0de`. +- `PACKAGE_TOKEN`: Gitea token with package write access. + +No downstream repository is triggered. PostgreSQL support is part of this +image, and derivative images such as phpPgAdmin rebuild on their own schedule. + +## PHP homepage Helm chart + +`helm/php-homepage` deploys this image with a CephFS-backed web root. Existing +sites should set `persistence.existingClaim`; in that mode the chart references +the PVC but deliberately does not render or own it. This keeps site content +independent of the Helm release and safe from `helm uninstall`. + +The chart can create a new RWX PVC when `existingClaim` is empty. Such PVCs +carry Helm's `keep` resource policy by default as an additional safeguard for +website content. + +Each successful `main` or scheduled image build also publishes a new chart to +the `feedc0de` Gitea Helm registry. Its `appVersion` is the unique image tag +built by that same workflow run. Site values do not override the image, so +updating the Helm repository and upgrading a release selects the image from the +new chart: + +```sh +helm repo add brunner https://code.brunner.ninja/api/packages/feedc0de/helm +helm repo update brunner +helm upgrade --install example-site brunner/php-homepage -f values.yaml +``` diff --git a/helm/php-homepage/Chart.yaml b/helm/php-homepage/Chart.yaml new file mode 100644 index 0000000..196d19e --- /dev/null +++ b/helm/php-homepage/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: php-homepage +description: Serve a PHP website from a persistent shared web root +type: application +version: 0.1.0 +appVersion: latest +home: https://code.brunner.ninja/feedc0de/php-apache +sources: + - https://code.brunner.ninja/feedc0de/php-apache diff --git a/helm/php-homepage/README.md b/helm/php-homepage/README.md new file mode 100644 index 0000000..ad57f68 --- /dev/null +++ b/helm/php-homepage/README.md @@ -0,0 +1,61 @@ +# PHP homepage chart + +This chart runs the unified `php-apache` image with a web root mounted at +`/srv/http`. It is intended for sites whose source and mutable application data +live on CephFS so the same files are available to the runtime pod and the +browser-based development container. + +Published chart packages set `appVersion` to the unique php-apache image tag +created in the same CI run. `image.tag` is empty by default and therefore uses +that `appVersion`. Keep site-specific values free of image overrides so a chart +upgrade also upgrades the runtime image. + +Add and refresh the Gitea chart repository with: + +```sh +helm repo add brunner https://code.brunner.ninja/api/packages/feedc0de/helm +helm repo update brunner +``` + +## Existing sites + +Set the existing RWX claim in the site's values file: + +```yaml +fullnameOverride: example-site + +persistence: + existingClaim: example-site +``` + +When `existingClaim` is set, the chart emits no PersistentVolumeClaim. Helm +therefore neither owns nor deletes the claim. Deleting the release removes the +workload but leaves all website files intact. + +The first installation can adopt a Deployment, Service, and Ingress previously +created by `kubectl apply`: + +```sh +helm upgrade --install example-site brunner/php-homepage \ + --namespace default \ + --values /path/to/example-site/values.yaml \ + --take-ownership \ + --wait +``` + +Review `helm template` or a server-side dry-run before adoption. The release +name and `fullnameOverride` must match the existing resource names. The chart's +selector remains the legacy `app: ` selector so Kubernetes does +not reject the Deployment because its selector is immutable. + +## New sites + +With `persistence.existingClaim` empty, the chart creates an RWX PVC using +`rook-cephfs`. The PVC has `helm.sh/resource-policy: keep` by default, so an +uninstall does not discard site content. Set `persistence.retain: false` only +when deleting the release should also delete its dynamically created claim. + +Ingress, TLS, probes, scheduling, storage size, and the image tag are all +configurable through `values.yaml`. TCP probes are used by default because +individual websites may redirect `/`, require authentication, or select their +own HTTP status behavior. diff --git a/helm/php-homepage/templates/NOTES.txt b/helm/php-homepage/templates/NOTES.txt new file mode 100644 index 0000000..dda3cfa --- /dev/null +++ b/helm/php-homepage/templates/NOTES.txt @@ -0,0 +1,13 @@ +{{- if .Values.persistence.existingClaim }} +Web content is mounted from existing PVC {{ .Values.persistence.existingClaim }}. +That PVC is referenced but is not managed by this Helm release. +{{- else if .Values.persistence.enabled }} +Web content is mounted from PVC {{ include "php-homepage.fullname" . }}. +{{- end }} + +{{- if .Values.ingress.enabled }} +Website hosts: +{{- range .Values.ingress.hosts }} + {{ .host }} +{{- end }} +{{- end }} diff --git a/helm/php-homepage/templates/_helpers.tpl b/helm/php-homepage/templates/_helpers.tpl new file mode 100644 index 0000000..c397506 --- /dev/null +++ b/helm/php-homepage/templates/_helpers.tpl @@ -0,0 +1,28 @@ +{{- define "php-homepage.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "php-homepage.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{- define "php-homepage.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "php-homepage.selectorLabels" -}} +app: {{ include "php-homepage.fullname" . }} +{{- end }} + +{{- define "php-homepage.labels" -}} +helm.sh/chart: {{ include "php-homepage.chart" . }} +{{ include "php-homepage.selectorLabels" . }} +app.kubernetes.io/name: {{ include "php-homepage.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ default .Chart.AppVersion .Values.image.tag | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} diff --git a/helm/php-homepage/templates/deployment.yaml b/helm/php-homepage/templates/deployment.yaml new file mode 100644 index 0000000..c8dcdc4 --- /dev/null +++ b/helm/php-homepage/templates/deployment.yaml @@ -0,0 +1,93 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "php-homepage.fullname" . }} + labels: + {{- include "php-homepage.labels" . | nindent 4 }} + {{- with .Values.deploymentAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicaCount }} + minReadySeconds: 5 + revisionHistoryLimit: 3 + strategy: + {{- toYaml .Values.strategy | nindent 4 }} + selector: + matchLabels: + {{- include "php-homepage.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "php-homepage.labels" . | nindent 8 }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 30 + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ include "php-homepage.fullname" . }} + image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + {{- if .Values.probes.startup.enabled }} + startupProbe: + tcpSocket: + port: http + periodSeconds: {{ .Values.probes.startup.periodSeconds }} + timeoutSeconds: {{ .Values.probes.startup.timeoutSeconds }} + failureThreshold: {{ .Values.probes.startup.failureThreshold }} + {{- end }} + {{- if .Values.probes.readiness.enabled }} + readinessProbe: + tcpSocket: + port: http + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + {{- end }} + {{- if .Values.probes.liveness.enabled }} + livenessProbe: + tcpSocket: + port: http + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.liveness.failureThreshold }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumeMounts: + - name: webroot + mountPath: {{ .Values.persistence.mountPath }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumes: + - name: webroot + persistentVolumeClaim: + claimName: {{ default (include "php-homepage.fullname" .) .Values.persistence.existingClaim }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/php-homepage/templates/ingress.yaml b/helm/php-homepage/templates/ingress.yaml new file mode 100644 index 0000000..2a4c41b --- /dev/null +++ b/helm/php-homepage/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "php-homepage.fullname" . }} + labels: + {{- include "php-homepage.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "php-homepage.fullname" $ }} + port: + name: http + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/php-homepage/templates/pvc.yaml b/helm/php-homepage/templates/pvc.yaml new file mode 100644 index 0000000..1f9c782 --- /dev/null +++ b/helm/php-homepage/templates/pvc.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "php-homepage.fullname" . }} + labels: + {{- include "php-homepage.labels" . | nindent 4 }} + {{- if .Values.persistence.retain }} + annotations: + helm.sh/resource-policy: keep + {{- end }} +spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 4 }} + {{- with .Values.persistence.storageClass }} + storageClassName: {{ . }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/helm/php-homepage/templates/service.yaml b/helm/php-homepage/templates/service.yaml new file mode 100644 index 0000000..c6a3865 --- /dev/null +++ b/helm/php-homepage/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "php-homepage.fullname" . }} + labels: + {{- include "php-homepage.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.port }} + protocol: TCP + targetPort: http + selector: + {{- include "php-homepage.selectorLabels" . | nindent 4 }} diff --git a/helm/php-homepage/values.schema.json b/helm/php-homepage/values.schema.json new file mode 100644 index 0000000..36bb8e5 --- /dev/null +++ b/helm/php-homepage/values.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["image", "persistence", "service", "ingress"], + "properties": { + "replicaCount": { "type": "integer", "minimum": 1 }, + "fullnameOverride": { "type": "string" }, + "image": { + "type": "object", + "required": ["repository", "tag", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "persistence": { + "type": "object", + "required": ["enabled", "existingClaim", "mountPath"], + "properties": { + "enabled": { "type": "boolean" }, + "existingClaim": { "type": "string" }, + "mountPath": { "type": "string", "minLength": 1 }, + "retain": { "type": "boolean" } + } + }, + "ingress": { + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { "type": "boolean" } + } + } + } +} diff --git a/helm/php-homepage/values.yaml b/helm/php-homepage/values.yaml new file mode 100644 index 0000000..5aca743 --- /dev/null +++ b/helm/php-homepage/values.yaml @@ -0,0 +1,72 @@ +replicaCount: 1 + +image: + repository: registry.brunner.ninja/feedc0de/php-apache + # Empty means the chart's appVersion. Published charts set appVersion to + # the exact image build tag produced in the same CI run. + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: + - name: quay-pull-secret + +nameOverride: "" +fullnameOverride: "" + +deploymentAnnotations: {} +podAnnotations: {} + +strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + +service: + type: ClusterIP + port: 80 + +persistence: + enabled: true + existingClaim: "" + mountPath: /srv/http + accessModes: + - ReadWriteMany + storageClass: rook-cephfs + size: 1Gi + retain: true + +ingress: + enabled: false + className: traefik + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + hosts: + - host: example.invalid + paths: + - path: / + pathType: Prefix + tls: [] + +probes: + startup: + enabled: true + periodSeconds: 2 + timeoutSeconds: 2 + failureThreshold: 60 + readiness: + enabled: true + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 3 + liveness: + enabled: true + periodSeconds: 15 + timeoutSeconds: 2 + failureThreshold: 3 + +resources: {} + +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/opcache.ini b/opcache.ini new file mode 100644 index 0000000..eda559f --- /dev/null +++ b/opcache.ini @@ -0,0 +1,12 @@ +; Keep timestamp validation enabled because WordPress updates code on its PVC +; without necessarily restarting Apache. +opcache.enable=1 +opcache.enable_cli=0 +opcache.memory_consumption=192 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=1 +opcache.revalidate_freq=2 +opcache.save_comments=1 +opcache.validate_permission=1 +opcache.restrict_api=/srv/http