{
  "schema_version": 1,
  "generated_at": "2026-10-22T00:00:00.000Z",
  "canonical_url": "https://openlogs.fyi/tools/zammad",
  "slug": "zammad",
  "name": "Zammad",
  "tagline": "Web-based helpdesk and customer support system.",
  "replaces": {
    "summary": "Paid helpdesk and customer support services, such as Zendesk.",
    "products": [
      {
        "slug": "zendesk",
        "name": "Zendesk",
        "vendor": "Zendesk",
        "url": "https://openlogs.fyi/alternatives/zendesk"
      }
    ]
  },
  "stack": [
    {
      "kind": "language",
      "name": "Ruby"
    },
    {
      "kind": "database",
      "name": "PostgreSQL"
    },
    {
      "kind": "database",
      "name": "Redis"
    }
  ],
  "deployment_methods": [
    "docker_compose"
  ],
  "requirements": {
    "min_ram_mb": null,
    "min_cpu_cores": null,
    "source_url": null
  },
  "license": {
    "spdx": "AGPL-3.0",
    "class": "osi",
    "source_url": "https://github.com/zammad/zammad/blob/e31c7d12aaa00469aeab66e788ef7c165c193e18/LICENSE"
  },
  "business_model": {
    "value": "unknown",
    "notes": "The README at the pinned commit does not state a business model, so it stays unknown.",
    "source_url": "https://github.com/zammad/zammad/blob/e31c7d12aaa00469aeab66e788ef7c165c193e18/README.md",
    "checked_at": "2026-10-22"
  },
  "difficulty": {
    "value": "hard",
    "reason": "10 services (4 database or broker: elasticsearch, memcached, postgres, redis), 5 volume mounts, 68 env values to set, no elevated host access. Heuristic score 9 of 11: hard."
  },
  "repository": {
    "owner": "zammad",
    "name": "zammad",
    "url": "https://github.com/zammad/zammad"
  },
  "compose_check": {
    "status": "config_ok",
    "validated_at": "2026-10-22",
    "summary": "config_ok on 2026-10-22"
  },
  "recipe": {
    "version": 1,
    "source_path": "docker-compose.yml",
    "source_repo": "zammad/zammad-docker-compose",
    "source_commit_sha": "16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1",
    "source_kind": "packaging_repo",
    "source_url": "https://github.com/zammad/zammad-docker-compose/blob/16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1/docker-compose.yml"
  },
  "install_steps": [
    {
      "order": 1,
      "kind": "prerequisite",
      "language": "text",
      "label": "Docker with the Compose plugin",
      "body": "A host with Docker and the Docker Compose plugin. This entry runs from a compose file.",
      "source_url": "https://github.com/zammad/zammad-docker-compose/blob/16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1/docker-compose.yml",
      "validation_status": "unvalidated"
    },
    {
      "order": 2,
      "kind": "compose",
      "language": "yaml",
      "label": "Save the compose file as docker-compose.yml",
      "body": "---\nx-shared:\n  zammad-service: &zammad-service\n    environment: &zammad-environment\n      MEMCACHE_SERVERS: ${MEMCACHE_SERVERS:-zammad-memcached:11211}\n      POSTGRESQL_DB: ${POSTGRES_DB:-zammad_production}\n      POSTGRESQL_HOST: ${POSTGRES_HOST:-zammad-postgresql}\n      POSTGRESQL_USER: ${POSTGRES_USER:-zammad}\n      POSTGRESQL_PASS: ${POSTGRES_PASS:-zammad}\n      POSTGRESQL_PORT: ${POSTGRES_PORT:-5432}\n      POSTGRESQL_OPTIONS: ${POSTGRESQL_OPTIONS:-?pool=50}\n      # The bundled zammad-postgresql service creates the database up front, and its\n      #   application role is deliberately not allowed to create databases. Set this to\n      #   'true' if you point Zammad at an external server and want it to create the\n      #   database itself - the configured role then needs the CREATEDB attribute.\n      POSTGRESQL_DB_CREATE: ${POSTGRESQL_DB_CREATE:-false}\n\n      # Redis standalone\n      REDIS_URL: ${REDIS_URL:-redis://zammad-redis:6379}\n      # Redis sentinel\n      REDIS_SENTINELS:\n      REDIS_SENTINEL_NAME:\n      REDIS_USERNAME:\n      REDIS_PASSWORD:\n      REDIS_SENTINEL_USERNAME:\n      REDIS_SENTINEL_PASSWORD:\n\n      S3_URL:\n      # Backup settings\n      BACKUP_DIR: \"${BACKUP_DIR:-/var/tmp/zammad}\"\n      BACKUP_TIME: \"${BACKUP_TIME:-03:00}\"\n      BACKUP_ON_START: \"${BACKUP_ON_START:-false}\"\n      HOLD_DAYS: \"${HOLD_DAYS:-10}\"\n      TZ: \"${TZ:-Europe/Berlin}\"\n      # Allow passing in these variables via .env:\n      AUTOWIZARD_JSON:\n      AUTOWIZARD_RELATIVE_PATH:\n      ELASTICSEARCH_ENABLED:\n      ELASTICSEARCH_SCHEMA:\n      ELASTICSEARCH_HOST:\n      ELASTICSEARCH_PORT:\n      ELASTICSEARCH_USER:\n      ELASTICSEARCH_PASS:\n      ELASTICSEARCH_NAMESPACE:\n      ELASTICSEARCH_REINDEX:\n      NGINX_PORT:\n      NGINX_CLIENT_MAX_BODY_SIZE:\n      NGINX_SERVER_NAME:\n      NGINX_SERVER_SCHEME:\n      RAILS_TRUSTED_PROXIES:\n      ZAMMAD_HTTP_TYPE:\n      ZAMMAD_FQDN:\n      ZAMMAD_WEB_CONCURRENCY:\n      ZAMMAD_MANAGE_SESSIONS_JOBS_WORKERS:\n      ZAMMAD_PROCESS_SESSIONS_JOBS_WORKERS:\n      ZAMMAD_PROCESS_SCHEDULED_JOBS_WORKERS:\n      ZAMMAD_PROCESS_DELAYED_JOBS_WORKERS:\n      ZAMMAD_PROCESS_DELAYED_JOBS_WORKER_THREADS:\n      ZAMMAD_PROCESS_DELAYED_AI_JOBS_WORKERS:\n      ZAMMAD_PROCESS_DELAYED_AI_JOBS_WORKER_THREADS:\n      ZAMMAD_PROCESS_DELAYED_COMMUNICATION_INBOUND_JOBS_WORKERS:\n      ZAMMAD_PROCESS_DELAYED_COMMUNICATION_INBOUND_JOBS_WORKER_THREADS:\n      ZAMMAD_OTRS_IMPORT_READ_TIMEOUT:\n      ZAMMAD_OTRS_IMPORT_TOTAL_TIMEOUT:\n      ZAMMAD_HTTP_OPEN_TIMEOUT:\n      ZAMMAD_HTTP_READ_TIMEOUT:\n      ZAMMAD_HTTP_TOTAL_TIMEOUT:\n      ZAMMAD_HTTP_AI_READ_TIMEOUT:\n      ZAMMAD_HTTP_AI_TOTAL_TIMEOUT:\n      ZAMMAD_HTTP_ELASTICSEARCH_READ_TIMEOUT:\n      ZAMMAD_HTTP_ELASTICSEARCH_TOTAL_TIMEOUT:\n      ZAMMAD_HTTP_ELASTICSEARCH_REINDEX_READ_TIMEOUT:\n      ZAMMAD_HTTP_ELASTICSEARCH_REINDEX_TOTAL_TIMEOUT:\n      ZAMMAD_HTTP_IMPORT_ATTACHMENT_READ_TIMEOUT:\n      ZAMMAD_HTTP_IMPORT_ATTACHMENT_TOTAL_TIMEOUT:\n      ZAMMAD_HTTP_WEBHOOK_READ_TIMEOUT:\n      ZAMMAD_HTTP_WEBHOOK_TOTAL_TIMEOUT:\n\n      # Allow disabling individual background services in the scheduler container.\n      ZAMMAD_PROCESS_SESSIONS_JOBS_DISABLE:\n      ZAMMAD_MANAGE_SESSIONS_JOBS_DISABLE:\n      ZAMMAD_PROCESS_SCHEDULED_JOBS_DISABLE:\n      ZAMMAD_PROCESS_DELAYED_JOBS_DISABLE:\n      ZAMMAD_PROCESS_DELAYED_AI_JOBS_DISABLE:\n      ZAMMAD_PROCESS_DELAYED_COMMUNICATION_INBOUND_JOBS_DISABLE:\n\n      ZAMMAD_GRAPHQL_INTROSPECTION:\n      ZAMMAD_AI_API_URL:\n      ZAMMAD_AI_TOKEN:\n      ZAMMAD_UI_BULK_BACKGROUND_UPDATE_THRESHOLD:\n      ZAMMAD_SETTING_TTL:\n      ZAMMAD_SAFE_MODE:\n      ZAMMAD_WEBSOCKET_SESSION_STORE_FORCE_FS_BACKEND:\n      ZAMMAD_RAILSSERVER_PORT:\n\n      # ZAMMAD_SESSION_JOBS_CONCURRENT is deprecated, please use ZAMMAD_PROCESS_SESSIONS_JOBS_WORKERS instead.\n      ZAMMAD_SESSION_JOBS_CONCURRENT:\n      # Variables used by ngingx-proxy container for reverse proxy creations\n      # for docs refer to https://github.com/nginx-proxy/nginx-proxy\n      VIRTUAL_HOST:\n      VIRTUAL_PORT:\n      # Variables used by acme-companion for retrieval of LetsEncrypt certificate\n      # for docs refer to https://github.com/nginx-proxy/acme-companion\n      LETSENCRYPT_HOST:\n      LETSENCRYPT_EMAIL:\n\n    image: ${IMAGE_REPO:-ghcr.io/zammad/zammad}:${VERSION:-7.2.2-0012}\n    init: true\n    restart: ${RESTART:-always}\n    volumes:\n      - zammad-backup:/var/tmp/zammad:ro # needed for waiting on restore operations\n      - zammad-storage:/opt/zammad/storage\n    depends_on:\n      zammad-memcached:\n        condition: service_healthy\n      zammad-postgresql:\n        condition: service_healthy\n      zammad-redis:\n        condition: service_healthy\n\nservices:\n  zammad-backup:\n    <<: *zammad-service\n    command: [\"zammad-backup\"]\n    volumes:\n      - zammad-backup:/var/tmp/zammad\n      - zammad-storage:/opt/zammad/storage\n    user: 0:0\n\n  zammad-elasticsearch:\n    image: elasticsearch:${ELASTICSEARCH_VERSION:-9.5.5}\n    restart: ${RESTART:-always}\n    volumes:\n      - elasticsearch-data:/usr/share/elasticsearch/data\n    environment:\n      discovery.type: single-node\n      xpack.security.enabled: \"false\"\n      ES_JAVA_OPTS: ${ELASTICSEARCH_JAVA_OPTS:--Xms1g -Xmx1g}\n\n  zammad-init:\n    <<: *zammad-service\n    command: [\"zammad-init\"]\n    restart: on-failure\n    user: 0:0\n\n  zammad-memcached:\n    command: memcached -m 256M\n    image: memcached:${MEMCACHE_VERSION:-1.6.45-alpine}\n    restart: ${RESTART:-always}\n    healthcheck:\n      test: [\"CMD\", \"nc\", \"-z\", \"127.0.0.1\", \"11211\"]\n      interval: 10s\n      timeout: 5s\n      start_period: 10s\n      retries: 5\n\n  zammad-nginx:\n    <<: *zammad-service\n    command: [\"zammad-nginx\"]\n    init: false\n    expose:\n      - \"${NGINX_PORT:-8080}\"\n    ports:\n      - \"${NGINX_EXPOSE_PORT:-8080}:${NGINX_PORT:-8080}\"\n    depends_on:\n      zammad-railsserver:\n        condition: service_healthy\n\n  zammad-postgresql:\n    # PostgreSQL tuning: dedicated variables for the settings the Zammad documentation\n    #   recommends to tune, plus POSTGRES_EXTRA_OPTS for any other setting as raw\n    #   -c flags - see .env.dist. Only variables that are set are passed to the server,\n    #   unset ones leave it completely untouched (including manual tuning in the data\n    #   volume). Compose word-splits the string into argv like a shell, so the\n    #   free-form POSTGRES_EXTRA_OPTS works without a shell wrapper.\n    command: >-\n      postgres\n      ${POSTGRES_MAX_CONNECTIONS:+-c max_connections=${POSTGRES_MAX_CONNECTIONS}}\n      ${POSTGRES_SHARED_BUFFERS:+-c shared_buffers=${POSTGRES_SHARED_BUFFERS}}\n      ${POSTGRES_TEMP_BUFFERS:+-c temp_buffers=${POSTGRES_TEMP_BUFFERS}}\n      ${POSTGRES_WORK_MEM:+-c work_mem=${POSTGRES_WORK_MEM}}\n      ${POSTGRES_MAX_STACK_DEPTH:+-c max_stack_depth=${POSTGRES_MAX_STACK_DEPTH}}\n      ${POSTGRES_EXTRA_OPTS:-}\n    # Docker caps /dev/shm at 64MB - the one PostgreSQL-adjacent knob a -c flag cannot\n    #   reach. Raise it together with shared_buffers/work_mem, otherwise parallel query\n    #   workers can fail with \"could not resize shared memory segment\".\n    shm_size: ${POSTGRES_SHM_SIZE:-64mb}\n    environment:\n      # The bootstrap role of the postgres image is always a superuser, so it is kept\n      #   separate from the role Zammad connects with and only used for administration.\n      #   Its password falls back to POSTGRES_PASS, so that hardening that one variable\n      #   does not silently leave a superuser behind on the default password.\n      POSTGRES_USER: ${POSTGRES_SUPERUSER:-postgres}\n      POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASS:-${POSTGRES_PASS:-zammad}}\n      # Consumed by the initdb hook below, to provision Zammad's unprivileged role\n      #   and the database it owns.\n      ZAMMAD_DB: ${POSTGRES_DB:-zammad_production}\n      ZAMMAD_DB_USER: ${POSTGRES_USER:-zammad}\n      ZAMMAD_DB_PASS: ${POSTGRES_PASS:-zammad}\n    image: postgres:${POSTGRES_VERSION:-17.11-alpine}\n    restart: ${RESTART:-always}\n    volumes:\n      - postgresql-data:/var/lib/postgresql/data\n    configs:\n      - source: postgresql-initdb\n        target: /docker-entrypoint-initdb.d/10-create-zammad-role.sh\n        mode: 0555\n    healthcheck:\n      # Connect over TCP as Zammad's own role rather than using pg_isready, which\n      #   reports success for an unknown role or database, and would also accept the\n      #   socket-only server that runs while the data directory is still initialising.\n      #   Dependent services wait for this healthcheck, so it has to establish that\n      #   the role and the database the initdb hook provisions exist and accept\n      #   connections.\n      #\n      # The password is supplied so that this keeps working where host connections\n      #   require authentication, for example with\n      #   POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256. With the image default the\n      #   loopback address is trusted, so the credentials are not verified there.\n      test:\n        - CMD-SHELL\n        - |-\n          PGPASSWORD=\"$${ZAMMAD_DB_PASS}\" \\\n          psql --no-password --quiet --output /dev/null --variable ON_ERROR_STOP=1 \\\n               --host 127.0.0.1 --username \"$${ZAMMAD_DB_USER}\" \\\n               --dbname \"$${ZAMMAD_DB}\" --command \"SELECT 1\"\n      interval: 10s\n      timeout: 5s\n      start_period: 60s\n      retries: 5\n\n  zammad-railsserver:\n    <<: *zammad-service\n    command: [\"zammad-railsserver\"]\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-sf\", \"http://127.0.0.1:${ZAMMAD_RAILSSERVER_PORT:-3000}\"]\n      interval: 30s\n      timeout: 5s\n      start_period: 120s\n      retries: 3\n\n  zammad-redis:\n    image: redis:${REDIS_VERSION:-8.10.2-alpine}\n    restart: ${RESTART:-always}\n    volumes:\n      - redis-data:/data\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 10s\n      timeout: 5s\n      start_period: 10s\n      retries: 5\n\n  zammad-scheduler:\n    <<: *zammad-service\n    command: [\"zammad-scheduler\"]\n\n  zammad-websocket:\n    <<: *zammad-service\n    command: [\"zammad-websocket\"]\n\nconfigs:\n  postgresql-initdb:\n    # Provisions the database that Zammad uses, owned by a dedicated login role.\n    #\n    # The postgres image unconditionally makes its bootstrap role a superuser. Zammad\n    #   does not need any superuser capability - it only owns its own database and\n    #   relies on the built-in plpgsql extension - so the application role is created\n    #   here as a plain login role, rather than reusing the bootstrap role.\n    #\n    # This runs only while an empty data directory is initialised. Installations whose\n    #   volume already exists keep the role layout they were created with, see the\n    #   README for how to migrate those.\n    #\n    # The content is inlined so that this file stays self-contained and can be\n    #   deployed as-is, for example by pasting it into Portainer. '$$' escapes Compose\n    #   interpolation, so those variables are expanded inside the container instead.\n    content: |\n      #!/bin/bash\n\n      set -o errexit\n      set -o pipefail\n\n      # initdb has already run by the time this hook does, so a rejected configuration\n      #   cannot be repaired by correcting it and starting again: the data directory is\n      #   populated and the entrypoint never processes this directory a second time.\n      #   Say so, rather than leaving the operator with a role or database that is\n      #   merely reported missing later on.\n      zammad_abort() {\n        echo \"$1\" >&2\n        echo \"Correct this, then remove the postgresql-data volume and start again. The\" >&2\n        echo \"  database directory is already initialised, so this will not run twice.\" >&2\n        exit 1\n      }\n\n      # Zammad must not reuse the bootstrap role, which is always a superuser. This\n      #   cannot be enforced on later starts: the only reliable signal is this\n      #   comparison, and checking the role's actual attributes instead would mark\n      #   every installation predating this change as unhealthy.\n      if [ \"$${ZAMMAD_DB_USER}\" = \"$${POSTGRES_USER}\" ]; then\n        zammad_abort \"POSTGRES_USER and POSTGRES_SUPERUSER must differ, the latter is a superuser.\"\n      fi\n\n      # The system databases exist already, so they would keep the bootstrap role as\n      #   their owner, and Zammad would run inside a database it does not own.\n      case \"$${ZAMMAD_DB}\" in\n        postgres | template0 | template1)\n          zammad_abort \"POSTGRES_DB must not be one of PostgreSQL's system databases.\"\n          ;;\n      esac\n\n      echo \"Creating the '$${ZAMMAD_DB_USER}' role and the '$${ZAMMAD_DB}' database…\"\n\n      # The identifiers and the password are passed as psql variables, so that psql\n      #   quotes them and no shell quoting can leak into the statements. The role owns\n      #   the database, which already carries every privilege it needs on it.\n      psql --variable ON_ERROR_STOP=1 \\\n           --username \"$${POSTGRES_USER}\" \\\n           --dbname \"$${POSTGRES_DB}\" \\\n           --variable role=\"$${ZAMMAD_DB_USER}\" \\\n           --variable pass=\"$${ZAMMAD_DB_PASS}\" \\\n           --variable db=\"$${ZAMMAD_DB}\" <<'EOSQL'\n      CREATE ROLE :\"role\" LOGIN PASSWORD :'pass';\n      CREATE DATABASE :\"db\" OWNER :\"role\";\n      EOSQL\n\nvolumes:\n  elasticsearch-data:\n    driver: local\n  postgresql-data:\n    driver: local\n  redis-data:\n    driver: local\n  zammad-backup:\n    driver: local\n  zammad-storage:\n    driver: local\n",
      "source_url": "https://github.com/zammad/zammad-docker-compose/blob/16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1/docker-compose.yml",
      "validation_status": "config_ok"
    },
    {
      "order": 3,
      "kind": "env",
      "language": "text",
      "label": "Set the values the compose file needs",
      "body": "These variables have no default in the compose file: AUTOWIZARD_JSON, AUTOWIZARD_RELATIVE_PATH, ELASTICSEARCH_ENABLED, ELASTICSEARCH_HOST, ELASTICSEARCH_NAMESPACE, ELASTICSEARCH_PASS, ELASTICSEARCH_PORT, ELASTICSEARCH_REINDEX and 60 more. Compose reads them from the shell or from a .env file next to the compose file.",
      "source_url": "https://github.com/zammad/zammad-docker-compose/blob/16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1/docker-compose.yml",
      "validation_status": "unvalidated"
    },
    {
      "order": 4,
      "kind": "shell",
      "language": "bash",
      "label": "Start it",
      "body": "docker compose up -d",
      "source_url": "https://github.com/zammad/zammad-docker-compose/blob/16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1/docker-compose.yml",
      "validation_status": "unvalidated"
    },
    {
      "order": 5,
      "kind": "verify",
      "language": "text",
      "label": "Open the app",
      "body": "Open http://localhost:8080. The compose file publishes host port 8080 for the zammad-nginx service.",
      "source_url": "https://github.com/zammad/zammad-docker-compose/blob/16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1/docker-compose.yml",
      "validation_status": "unvalidated"
    }
  ],
  "notes": [
    {
      "kind": "gotcha",
      "title": "The compose file publishes host ports",
      "body": "Host ports: 8080 (zammad-nginx). A port without a host address listens on all host network interfaces.",
      "source_url": "https://github.com/zammad/zammad-docker-compose/blob/16f43d8d5c7a31dbc05afe1bd4a2942d037c7da1/docker-compose.yml",
      "checked_at": "2026-10-22"
    }
  ],
  "signals": {
    "captured_at": "2026-10-22",
    "stars": 6025,
    "forks": 1080,
    "open_issues": 471,
    "last_push": "2026-10-22",
    "days_since_push": 0,
    "latest_release": null,
    "latest_release_date": null,
    "days_since_release": null
  },
  "links": {
    "html": "https://openlogs.fyi/tools/zammad",
    "markdown": "https://openlogs.fyi/tools/zammad.md",
    "json": "https://openlogs.fyi/tools/zammad.json"
  },
  "updated_at": "2026-10-22"
}
