Install ASE

ASE is shipped as the Docker image clinuxfr/ase-ops. A production-like deployment uses four services: web, app, scheduler, and database.

Prerequisites

Install Docker and Docker Compose, choose a public URL, and prepare strong secrets for Symfony and MariaDB.

Minimal .env

ASE_IMAGE=clinuxfr/ase-ops
ASE_VERSION=latest
HTTP_PORT=8080
PUBLIC_SCHEME=http
DEFAULT_URI=http://localhost:8080

# Optional. Leave empty to let administrators choose the display timezone in the UI.
APP_TIMEZONE=

APP_SECRET=replace-with-a-long-random-value
MARIADB_DATABASE=ase
MARIADB_USER=ase
MARIADB_PASSWORD=replace-with-a-strong-password
MARIADB_ROOT_PASSWORD=replace-with-a-different-strong-password

MAILER_DSN=
ASE_MAIL_FROM=
ASE_MAIL_FROM_NAME=
ASE_MATTERMOST_WEBHOOK_URL=
ASE_MATTERMOST_USERNAME=
ASE_EXECUTION_LOG_DIR=/app/var/execution-logs

Docker Compose file

name: ase

services:
  app:
    image: ${ASE_IMAGE:?ASE_IMAGE is required}:${ASE_VERSION:-latest}
    environment:
      APP_ENV: prod
      APP_DEBUG: 0
      APP_SECRET: ${APP_SECRET:?APP_SECRET is required}
      APP_TIMEZONE: ${APP_TIMEZONE:-}
      DEFAULT_URI: ${DEFAULT_URI:-http://localhost}
      MAILER_DSN: ${MAILER_DSN:-}
      DATABASE_HOST: database
      DATABASE_PORT: 3306
      DATABASE_NAME: ${MARIADB_DATABASE:-ase}
      DATABASE_USER: ${MARIADB_USER:-ase}
      DATABASE_PASSWORD: ${MARIADB_PASSWORD:?MARIADB_PASSWORD is required}
    depends_on:
      database:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "php", "bin/console", "app:health:check", "--quiet"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - ./public_assets:/app/public
      - ./execution_logs:/app/var/execution-logs
    restart: unless-stopped

  web:
    image: nginx:1.28-alpine
    depends_on:
      app:
        condition: service_healthy
    ports:
      - "${HTTP_PORT:-8080}:80"
    volumes:
      - ./public_assets:/app/public:ro
    restart: unless-stopped

  scheduler:
    image: ${ASE_IMAGE:?ASE_IMAGE is required}:${ASE_VERSION:-latest}
    environment:
      APP_ENV: prod
      APP_DEBUG: 0
      APP_SECRET: ${APP_SECRET:?APP_SECRET is required}
      APP_TIMEZONE: ${APP_TIMEZONE:-}
      DEFAULT_URI: ${DEFAULT_URI:-http://localhost}
      DATABASE_HOST: database
      DATABASE_PORT: 3306
      DATABASE_NAME: ${MARIADB_DATABASE:-ase}
      DATABASE_USER: ${MARIADB_USER:-ase}
      DATABASE_PASSWORD: ${MARIADB_PASSWORD:?MARIADB_PASSWORD is required}
      SKIP_MIGRATIONS: 1
    depends_on:
      app:
        condition: service_healthy
    command: ["php", "bin/console", "app:scheduler:work", "--no-interaction"]
    volumes:
      - ./execution_logs:/app/var/execution-logs
    restart: unless-stopped

  database:
    image: mariadb:11.8
    environment:
      MARIADB_DATABASE: ${MARIADB_DATABASE:-ase}
      MARIADB_USER: ${MARIADB_USER:-ase}
      MARIADB_PASSWORD: ${MARIADB_PASSWORD:?MARIADB_PASSWORD is required}
      MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:?MARIADB_ROOT_PASSWORD is required}
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
    volumes:
      - ./database_data:/var/lib/mysql
    restart: unless-stopped

First start

docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 app

Migrations

docker compose exec app php bin/console doctrine:migrations:migrate --no-interaction

Scheduler health

The scheduler writes its heartbeat to execution_logs/.scheduler-heartbeat.json. Both the app and scheduler containers must mount the same execution_logs path.

docker compose ps scheduler
docker compose logs --tail=100 scheduler
ls -la execution_logs

Reverse proxy and HTTPS

Set DEFAULT_URI to the public URL used in links and notifications. Behind a TLS reverse proxy, set PUBLIC_SCHEME=https and forward host and scheme headers to nginx.

Updating the image

docker compose pull
docker compose up -d
docker compose exec app php bin/console doctrine:migrations:migrate --no-interaction

Troubleshooting

Check app health, scheduler logs, database health, and the shared volumes first. Most startup issues come from missing secrets, database readiness, or mismatched volume mounts.