<!--
title: Install with Docker
section: Installation
order: 13
keywords: docker, docker compose, container, mariadb, scheduler, volumes, logs, backup, mysqldump, port 8080, env.docker, demo lock, nightly reset
-->
# Install with Docker

Docker runs POSMaster and its database in ready-made containers, so you do not install PHP, Apache or MySQL yourself. It is a good choice for a VPS, a test server, or a computer that already has Docker.

You need **Docker** with **Docker Compose v2** (the `docker compose` command). On Windows and Mac, install Docker Desktop. On Linux, install Docker Engine and the Compose plugin.

## Quick start

Open a terminal in the folder that contains `docker-compose.yml`, then run these two commands.

1. Build and start everything:

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

The first build takes a few minutes, because Docker downloads PHP, Apache and MariaDB and installs the PHP extensions.

2. Install POSMaster. Pick one:

```bash
# Demo shop with sample products and sales
docker compose exec -u www-data app php artisan pos:install --demo

# Or a clean shop (asks for your Super Admin name, email and password)
docker compose exec -u www-data app php artisan pos:install
```

3. Open **http://localhost:8080** and sign in.

With `--demo` the Super Admin is `superadmin@posmaster.test` with the password `password`.

> **Warning:** `pos:install --demo` **wipes the database** before loading the demo shop. Never run it on a shop with real data.

> **Note:** If the second command says it cannot connect to the database, wait 20 seconds and try again. The database needs a moment to start on the first run. The app waits for it automatically.

> **Warning:** Always run `php artisan` in the container with `-u www-data`, as shown. Without it, files are created as root, the web server can no longer write its cache, and pages show an error.

## What Docker starts

`docker-compose.yml` defines three services:

| Service | What it does |
|---------|--------------|
| `app` | PHP 8.2 with Apache. Serves POSMaster on port 8080. On start it creates the app key, waits for the database, and creates the storage link. |
| `scheduler` | Runs `php artisan schedule:work`. It handles the nightly demo reset when you turn it on. |
| `db` | MariaDB 10.11 with the utf8mb4 character set and `utf8mb4_unicode_ci` collation. |

The app is configured through environment variables set in the compose file, so you do **not** need a `.env` file inside the container. `APP_ENV` is `production` and `APP_DEBUG` is `false`. Logs go to the container output.

You do not need to create the app key. The first start creates a random key and keeps it in the `storage` volume, so it stays the same as long as that volume exists.

## Change the settings (.env.docker)

The defaults work for a first try. For anything real, change at least the passwords. Create your own settings file:

```bash
cp .env.docker.example .env.docker
```

Edit `.env.docker`:

```ini
POS_PORT=8080
POS_APP_URL=http://localhost:8080
POS_DB_DATABASE=posmaster
POS_DB_USERNAME=posmaster
POS_DB_PASSWORD=change-me
POS_DB_ROOT_PASSWORD=change-me-too
POS_DEMO_LOCK=false
POS_DEMO_RESET_NIGHTLY=false
```

Then start with the file:

```bash
docker compose --env-file .env.docker up -d --build
```

| Setting | Meaning |
|---------|---------|
| `POS_PORT` | The port on your computer. Use `80` to open the app without a port number. |
| `POS_APP_URL` | The address people type. Set it to your real domain, for example `https://shop.example.com`. |
| `POS_DB_DATABASE`, `POS_DB_USERNAME`, `POS_DB_PASSWORD` | The database name, user and password |
| `POS_DB_ROOT_PASSWORD` | The password of the database root user |
| `POS_DEMO_LOCK` | `true` forces demo mode on and hides the switch. See below. |
| `POS_DEMO_RESET_NIGHTLY` | `true` resets the demo data every night at 00:00 |

> **Warning:** The database password is saved in the database volume the first time it is created. If you change `POS_DB_PASSWORD` later, the existing database does not follow. Set the passwords **before** the first start. To change them afterwards you must either change them inside the database or remove the database volume, which deletes all data.

> **Tip:** Use the same `--env-file .env.docker` option every time you run `docker compose up`, `down` or `build`. Otherwise Compose falls back to the defaults.

## Public demo server

To run a public demo that visitors cannot damage:

1. Put these in `.env.docker`:

```ini
POS_DEMO_LOCK=true
POS_DEMO_RESET_NIGHTLY=true
```

2. Start the containers and install the demo:

```bash
docker compose --env-file .env.docker up -d --build
docker compose exec -u www-data app php artisan pos:install --demo
```

With `POS_DEMO_LOCK=true`, demo mode is always on. Nobody, not even the Super Admin, can switch it off or change protected settings. With `POS_DEMO_RESET_NIGHTLY=true`, the `scheduler` service wipes and reloads the demo data at 00:00 every night.

## Data and volumes

Docker keeps your data in two named volumes. They survive restarts and rebuilds.

| Volume | Holds |
|--------|-------|
| `db` | The MariaDB database files |
| `storage` | Uploaded pictures, logs, the app key and the cache |

Compose adds your folder name in front, so the real names look like `pointofsales_db`. List them with `docker volume ls`.

> **Warning:** `docker compose down -v` **deletes the volumes and all your data**. Plain `docker compose down` is safe.

## Look at the logs

```bash
# Follow the app log (press Ctrl+C to stop following)
docker compose logs -f app

# Database and scheduler
docker compose logs -f db
docker compose logs -f scheduler
```

Application errors are written to the app container's output, because the log channel is `stderr`.

## Stop, start and restart

```bash
docker compose stop        # stop, keep everything
docker compose start       # start again
docker compose restart app # restart one service
docker compose down        # remove the containers, keep the data
docker compose ps          # see what is running
```

The containers are set to `restart: unless-stopped`, so they come back after a server reboot.

## Back up the database

Create a SQL dump of the database on your computer:

```bash
docker compose exec db sh -c 'mariadb-dump -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"' > posmaster-backup.sql
```

This uses the user, password and database name that are already set inside the `db` container, so you do not have to type them. Put the date in the file name to keep several copies.

Also copy the uploaded pictures:

```bash
docker compose cp app:/var/www/html/storage/app/public ./backup-public
```

## Restore a backup

1. Make sure the containers are running.
2. Load the dump:

```bash
docker compose exec -T db sh -c 'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"' < posmaster-backup.sql
```

3. Copy the pictures back if you need them:

```bash
docker compose cp ./backup-public/. app:/var/www/html/storage/app/public
```

See [Update, backup and restore](update-backup.html) for a full backup plan.

## Update to a new version

1. Make a backup first (see above).
2. Replace the project files with the new version. Keep your `.env.docker`.
3. Rebuild and restart:

```bash
docker compose --env-file .env.docker up -d --build
```

4. Run the database migrations and clear the cache:

```bash
docker compose exec -u www-data app php artisan migrate --force
docker compose exec -u www-data app php artisan optimize:clear
```

Your data is safe, because it lives in the volumes and not in the image.

## Useful commands

```bash
# Open a shell inside the app container
docker compose exec app sh

# Run any artisan command
docker compose exec -u www-data app php artisan about

# Reset the demo data now (only while demo mode is on)
docker compose exec -u www-data app php artisan pos:demo-reset
```

## Troubleshooting Docker

- **"Database not reachable." in the app log.** The app waited two minutes and the database never answered. Check `docker compose logs db`. A wrong password in `.env.docker` after the first start is the usual cause. See [Troubleshooting](troubleshooting.html).
- **Port 8080 is already in use.** Set a different `POS_PORT`, for example `8081`, and update `POS_APP_URL` to match.
- **Pictures do not show.** Check that `POS_APP_URL` is exactly the address in your browser.
- **Slow on Windows.** Use Docker Desktop with the WSL 2 backend.
