> For the complete documentation index, see [llms.txt](https://docs.uxwizz.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.uxwizz.com/installation/docker/via-docker-compose.md).

# Via Docker Compose

Run UXWizz, its database, and scheduled tasks with Docker Compose.

You need:

* A server with Docker and the Compose plugin, using Linux containers.
* Terminal access with permission to create containers and volumes. These commands use Bash and OpenSSL.
* A hostname and an HTTPS reverse proxy for public access.

## Video walkthrough

[![Create the Compose files, start UXWizz and add the tracking code](https://www.uxwizz.com/videos/uxwizz-install-docker-guide-v15.webp?v=6451777f6072b527)](https://www.uxwizz.com/videos/uxwizz-install-docker-guide-v15.mp4?v=5231e3979ccaa775)

[Watch the Docker Compose installation video](https://www.uxwizz.com/videos/uxwizz-install-docker-guide-v15.mp4?v=5231e3979ccaa775). [English captions](https://www.uxwizz.com/videos/uxwizz-install-docker-guide-v15.en.vtt?v=7cd5c3a7baf0543f) are also available.

## Running UXWizz via docker compose

1. Create a private working directory outside your web root:

   ```bash
   umask 077
   mkdir uxwizz
   cd uxwizz
   ```
2. Copy and run this command to create `.env` with random passwords and an application key:

   <pre class="language-bash" data-title="Create .env"><code class="lang-bash">(
     umask 077
     set -euC
     db_password=$(openssl rand -hex 32)
     root_password=$(openssl rand -hex 32)
     app_key=$(openssl rand -base64 32)
     printf 'UXWIZZ_DB_PASSWORD=%s\nUXWIZZ_DB_ROOT_PASSWORD=%s\nUXWIZZ_APP_KEY=%s\n' \
       "$db_password" "$root_password" "$app_key" > .env
   )
   </code></pre>

   The command keeps the values private and stops if `.env` already exists. Do not share this file in support messages.
3. Save this as `compose.yml` in the same directory:

<details>

<summary>Copy the complete compose.yml file</summary>

{% code title="compose.yml" %}

```yaml
x-uxwizz-environment: &uxwizz-environment
  UXWIZZ_DB_HOST: db
  UXWIZZ_DB_NAME: "${UXWIZZ_DB_NAME:-uxwizz}"
  UXWIZZ_DB_USER: "${UXWIZZ_DB_USER:-uxwizz}"
  UXWIZZ_DB_PASSWORD: "${UXWIZZ_DB_PASSWORD:?Set UXWIZZ_DB_PASSWORD}"
  UXWIZZ_APP_KEY: "${UXWIZZ_APP_KEY:?Set UXWIZZ_APP_KEY}"
  UXWIZZ_PUBLIC_SERVER_URL: "${UXWIZZ_PUBLIC_SERVER_URL:-}"
  UXWIZZ_TRUST_PROXY: "${UXWIZZ_TRUST_PROXY:-0}"

services:
  webserver:
    image: "uxwizz/uxwizz-webserver:${UXWIZZ_IMAGE_TAG:-10}"
    restart: unless-stopped
    ports:
      - "127.0.0.1:${UXWIZZ_HTTP_PORT:-8000}:80"
    environment: *uxwizz-environment
    volumes:
      - html:/var/www/html
    depends_on:
      db:
        condition: service_healthy

  scheduler:
    image: "uxwizz/uxwizz-webserver:${UXWIZZ_IMAGE_TAG:-10}"
    restart: unless-stopped
    command: ["scheduler"]
    healthcheck:
      disable: true
    environment: *uxwizz-environment
    volumes:
      - html:/var/www/html
    depends_on:
      db:
        condition: service_healthy
      webserver:
        condition: service_started

  db:
    image: mariadb:11.8
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: "${UXWIZZ_DB_NAME:-uxwizz}"
      MARIADB_USER: "${UXWIZZ_DB_USER:-uxwizz}"
      MARIADB_PASSWORD: "${UXWIZZ_DB_PASSWORD}"
      MARIADB_ROOT_PASSWORD: "${UXWIZZ_DB_ROOT_PASSWORD:?Set UXWIZZ_DB_ROOT_PASSWORD}"
      MARIADB_ROOT_HOST: localhost
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      start_period: 10s
      interval: 10s
      timeout: 5s
      retries: 12

volumes:
  html:
  mysql_data:
```

{% endcode %}

</details>

4. Validate the configuration, then start the services:

   ```bash
   docker compose config --quiet
   docker compose up -d
   docker compose ps --format 'table {{.Service}}\t{{.Status}}'
   ```

   All three services should be running. The database should show **healthy**.

{% hint style="info" %}
`config --quiet` checks for missing settings without printing secrets.
{% endhint %}

## Accessing UXWizz

### Connect your hostname

1. Configure your HTTPS reverse proxy. Replace `stats.example.com` with your hostname.

   | Setting        | Value                       |
   | -------------- | --------------------------- |
   | Public address | `https://stats.example.com` |
   | Forward to     | `http://127.0.0.1:8000`     |

   Keep port **8000** private. All public traffic must pass through your proxy. Configure it to:

   * Replace forwarded client-IP and protocol headers with values it determines.
   * Preserve the `Host` header and forward `Authorization`.
2. Add these lines to `.env`, using your hostname:

   <pre class="language-dotenv" data-title=".env" data-overflow="wrap"><code class="lang-dotenv">UXWIZZ_PUBLIC_SERVER_URL=https://stats.example.com/server
   UXWIZZ_TRUST_PROXY=1
   </code></pre>
3. Apply the changes:

   ```bash
   docker compose up -d
   ```

   Use `up -d` after editing `.env`; `restart` does not load new values.

<details>

<summary>My reverse proxy runs in another container</summary>

Connect the proxy to the same Docker network as `webserver`. Forward requests to `http://webserver:80` instead of the host address above.

Inside a container, `127.0.0.1` points to that container itself.

</details>

<details>

<summary>I only want to try UXWizz locally</summary>

Skip the hostname and proxy settings. Open <http://localhost:8000/server/install.php> in a browser on the Docker host.

Use a separate test password. Set up HTTPS before making UXWizz publicly accessible.

</details>

### Complete setup

{% hint style="warning" %}
Check that HTTPS works before entering passwords on a public server.
{% endhint %}

1. Open the setup page, using your hostname:

   <pre class="language-url" data-overflow="wrap"><code class="lang-url">https://stats.example.com/server/install.php
   </code></pre>
2. Create an administrator password of at least 12 characters, then sign in as **admin**. The database is already configured.
3. [Add the tracking code](/installation/adding-the-tracking-code.md) to your website.
4. Visit your website and check that the visit appears in **Visitors**.

### Check scheduled tasks

Open **Settings → Scheduled Tasks**. Check **Next run** for active tasks, then check **Runs** after a task is due.

An empty history is normal until an enabled task is due. The scheduler starts checking after browser setup is complete.

### Troubleshooting and updates

Run these commands on the server:

```bash
docker compose ps
docker compose logs --tail 100 db webserver scheduler
```

* **Database connection error:** check the shared application credentials. Changing `.env` does not change passwords inside an existing MariaDB volume. Restore the correct settings or have the database administrator rotate them deliberately.
* **No task history:** complete setup and check **Next run** in **Settings → Scheduled Tasks**. If an active task is overdue, inspect scheduler logs. A stopped container needs server access.
* **Permission error:** check the affected volume and PHP ownership. Do not use world-writable permissions.
* **Address already in use:** choose an unused `UXWIZZ_HTTP_PORT` and update the reverse-proxy target.

<details>

<summary>Stop or update UXWizz</summary>

Use `docker compose stop` to pause the services. Use `docker compose down` to remove the containers while keeping named volumes.

**Do not add `-v`: it deletes the stored data.**

Back up `.env`, the `html` volume, and a consistent database backup before updating. Keep the Compose project name and volume names. Update application files through **Settings → Updates**; pulling a new image does not replace files already in `html`. Do not switch an existing standalone container to Compose without a planned migration.

</details>

<details>

<summary>Advanced: change the default settings</summary>

Add only the settings you need to `.env`:

| Setting            | Default  | When to change it                                              |
| ------------------ | -------- | -------------------------------------------------------------- |
| `UXWIZZ_HTTP_PORT` | `8000`   | The host port is already in use. Update your proxy target too. |
| `UXWIZZ_IMAGE_TAG` | `10`     | You need a specific published image tag.                       |
| `UXWIZZ_DB_NAME`   | `uxwizz` | You need another database name.                                |
| `UXWIZZ_DB_USER`   | `uxwizz` | You need another database user.                                |

Database settings initialize a new database only. Editing `.env` does not rename an existing database or change its users and passwords.

</details>

## Tip: Using Bind Volumes

To use host folders with the Compose setup above, follow the steps below. Run the commands beside `compose.yml`, in a dedicated directory outside your web root. This needs server access.

### Prepare the application folder for a new installation

Create an empty application folder:

```bash
mkdir ./html
```

Then add the mounts in the next section. On first startup, the webserver image copies the application into the empty `./html` bind mount. Manual copying is optional for this new application folder. Separate PHP/Apache configuration mounts need the [preparation described below](#bind-mount-php-apache-or-database-files).

For an existing named volume, use the migration steps below. Use a new empty folder for a new installation; do not delete files from an existing folder.

<details>

<summary>Optional: copy the application before first startup</summary>

Use this if you need the application files on the host before the first startup. Run it instead of the `mkdir` command above, using the **same image tag as your Compose file**:

```bash
mkdir ./html &&
seed=$(docker create uxwizz/uxwizz-webserver:10) &&
docker cp "$seed":/opt/uxwizz/. ./html &&
docker rm -v "$seed" &&
test -f ./html/server/install.php && test -f ./html/.htaccess
```

The final check must succeed. The `/.` copy includes hidden files such as `.htaccess`. If `html` already exists, the commands stop without changing it. Inspect that folder before continuing.

The temporary container is never started. `docker rm -v "$seed"` removes only that container and its unused anonymous volume. If copying fails, keep the partial folder for diagnosis and remove the temporary container when finished.

</details>

### Use the folder in Compose

Replace `html:/var/www/html` with `./html:/var/www/html` in **both** services. Keep the other service settings:

```yaml
services:
  webserver:
    volumes:
      - ./html:/var/www/html
  scheduler:
    volumes:
      - ./html:/var/www/html
```

This is a fragment to merge into your existing file, not a complete Compose file. The scheduler must see the same application files as the web server.

```bash
docker compose config --quiet
docker compose up -d
docker compose ps
```

Check browser setup and **Settings → Scheduled Tasks** as described above. The image sets application ownership to `www-data` and directory/file modes to `0750`/`0640` on startup. This also changes the host folder's ownership and permissions on Linux.

### Move an existing installation from a named volume

<details>

<summary>Migration steps and rollback</summary>

Use the **existing container's files**, including its configuration and installed marker. Do not seed an existing installation from a clean image.

1. Back up the application volume, `.env`, and database. Confirm you can restore them. Plan a short tracking interruption.
2. Stop application writes and copy the files from the stopped web container:

   ```bash
   mkdir ./html &&
   docker compose stop webserver scheduler &&
   webserver=$(docker compose ps --all --quiet webserver) &&
   test -n "$webserver" &&
   docker cp "$webserver":/var/www/html/. ./html &&
   test -f ./html/server/install.php && test -f ./html/.htaccess
   ```
3. Check that the copy includes the original configuration and `server/storage/installed.php`. Keep the original named volume and its declaration for rollback.
4. Change the HTML mount in both services, validate Compose, and start them as above. Confirm sign-in, existing reports, a new visit, and task history. If setup asks you to create a new administrator, stop: the existing installation was not copied correctly.
5. If the checks fail, stop both services, restore their original named-volume mounts, and start them again. Keep the failed copy for diagnosis. Visits received after the switch may need recovery; do not merge the two folders blindly.

</details>

### Bind-mount PHP, Apache, or database files

<details>

<summary>Prepare custom configuration and database mounts</summary>

You can also keep these files on the host. Prepare each source before adding its mount.

Copy the supplied PHP settings and default Apache site from another temporary, unstarted webserver container:

```bash
mkdir ./php-config ./sites-enabled ./apache_logs &&
seed=$(docker create uxwizz/uxwizz-webserver:10) &&
docker cp "$seed":/usr/local/etc/php/conf.d/uxwizz.ini ./php-config/uxwizz.ini &&
docker cp "$seed":/etc/apache2/sites-available/000-default.conf ./sites-enabled/000-default.conf &&
docker rm -v "$seed"
```

Then add the mounts you need to `webserver.volumes`:

```yaml
- ./php-config/uxwizz.ini:/usr/local/etc/php/conf.d/uxwizz.ini:ro
- ./sites-enabled:/etc/apache2/sites-enabled:ro
- ./apache_logs:/var/log/apache2
```

Also mount the PHP file in `scheduler` if its CLI settings need to match. For an existing deployment, copy its actual configuration instead of the image defaults. Apache's `sites-enabled` directory can contain symlinks; copy each enabled site's target as a regular file so links do not point to missing host files. Do not mount a missing `php.ini` path: Docker's short mount syntax can create a directory where PHP expects a file.

Validate edited configuration before restarting:

```bash
docker compose run --rm --no-deps --entrypoint apache2ctl webserver configtest
```

For a **new, empty database only**, create `./mysql_data` and replace `mysql_data:/var/lib/mysql` in `db.volumes` with `./mysql_data:/var/lib/mysql`. MariaDB initializes that directory and may change its ownership. For an existing database, use a verified database backup and restore procedure, or an administrator's offline physical migration for the exact database version. Copying a running database's files is not a consistent backup.

See [Docker's bind-mount reference](https://docs.docker.com/engine/storage/bind-mounts/) for host-path behavior and platform-specific permissions.

</details>
