Skip to content

Latest commit

 

History

History
373 lines (284 loc) · 19.2 KB

File metadata and controls

373 lines (284 loc) · 19.2 KB

Deploying TicketsCAD NewUI with Docker

This is the fastest way to stand up a complete TicketsCAD NewUI instance — application and database — on any machine with Docker. One command builds the app image, starts MariaDB, installs the schema, creates an admin account, and serves the dispatch console.

Who this is for: anyone evaluating TicketsCAD or running it on a single host (a home lab, a squad's server, a cloud VM). For a hardened, internet-facing deployment, also read the "Production notes" section at the end.


1. Prerequisites

  • Docker Engine 24+ and the Docker Compose plugin (docker compose, not the older docker-compose). On Debian/Ubuntu:
    curl -fsSL https://get.docker.com | sudo sh
    sudo usermod -aG docker "$USER"   # log out/in so you can run docker without sudo
  • ~2 GB free disk for the images, plus room for your data.
  • git to clone the repository.

That's it — you do not need PHP, Composer, or MariaDB installed on the host. Everything runs inside containers.


2. Quick start

# 1. Get the code
git clone https://github.com/openises/TicketsCAD.git ticketscad
cd ticketscad

# 2. Create your environment file and set passwords
cp .env.example .env
nano .env          # change NEWUI_DB_PASS and DB_ROOT_PASSWORD at minimum

# 3. Build and start (app + database)
docker compose up -d --build

# 4. Watch the first-run install finish (schema + admin user)
docker compose logs -f app

On first start the app container waits for the database, installs the full schema, applies every migration, and creates the admin account. When you see:

[entrypoint]  Install complete. Sign in at http://localhost:8081
[entrypoint] Starting Apache...

open http://localhost:8081 in your browser.

Your admin password

  • If you set ADMIN_PASSWORD in .env, use that.
  • If you left it blank, a random one was generated — find it in the log:
    docker compose logs app | grep -iE "password|temp"
    You'll be prompted to choose your own password on first login.

3. Configuration reference

All settings are environment variables in .env (read automatically by docker compose):

Variable Default Purpose
NEWUI_DB_NAME newui Database name (created automatically).
NEWUI_DB_USER newui Database user.
NEWUI_DB_PASS newui Database password — change this.
DB_ROOT_PASSWORD change-me-root MariaDB root password — change this.
NEWUI_PORT 8081 Host port the app is published on (http://localhost:<port>).
NEWUI_BASE_URL http://localhost:8081 Public URL used to build links/cookies. Set to your https:// host in production.
ADMIN_USER admin First-run admin username.
ADMIN_EMAIL admin@example.invalid First-run admin email.
ADMIN_PASSWORD (blank → auto-gen) First-run admin password. Blank = generated + printed to the log.
NEWUI_SEED_DEMO false true seeds demo incident types + sample units/facilities.

After editing .env, apply changes with docker compose up -d (recreates the containers). Note that ADMIN_* and NEWUI_SEED_DEMO only take effect on the first install (an empty database) — they don't rotate an existing admin's password.


4. Data persistence

Your data lives in named Docker volumes, so it survives docker compose down and image rebuilds:

Volume Mounted at Holds
db_data /var/lib/mysql The entire database.
app_uploads /var/www/html/uploads Attachments, photos, uploaded files.
app_cache /var/www/html/cache Weather-overlay tiles and NWS lookups.
app_tile_cache /var/www/tile-cache Basemap tiles fetched by api/tile-proxy.php. Outside /var/www/html — this cache records which map areas the install has viewed, which inside the webroot would be readable without logging in. Regenerable, but keep it on a volume: a rebuild that empties it makes the install re-fetch every tile at once, which is the load spike tile providers ask us not to cause.
app_geocode_cache /var/www/geocode-cache Address-lookup cache (inc/geocode.php). Outside /var/www/html, same reasoning as app_tile_cache; regenerable but worth keeping on a volume so a rebuild doesn't re-hit the geocoding provider for every cached address at once.
app_backups /var/www/backups Archives written by tools/backup_run.php and Settings → Backup / Maintenance. Outside /var/www/html — that is the Apache DocumentRoot, and an archive inside it was downloadable by anyone who guessed the filename (v4.2.3).
app_keys /var/www/keys 2FA + RSA field-encryption keys (kept out of the webroot), and — in its tts/ subdirectory — the text-to-speech API keys saved under Settings → Voice & Speech.
app_zello_audio /var/www/zello-audio Zello voice-message recordings (GHSA-x9x6-w4fg-pmcc). Outside /var/www/html, mounted on BOTH the app service and the zello-proxy service (the proxy is what actually writes recordings). Irreplaceable, not regenerable — this is precious data, not a cache.

Anything NOT on this list lives in the container's writable layer and is destroyed by docker compose up -d --build, which replaces the container. That is why app_backups exists: without it, a backup taken with docker compose exec app php tools/backup_run.php --force was deleted by the very next command of the documented upgrade — in the one scenario the backup exists for.

Back up db_data and app_keys together. The keys decrypt data stored in the database; a database restored without its matching keys cannot read encrypted fields (2FA secrets, encrypted form fields).

One-time step if your compose file predates app_backups

Adding the volume does not import what was already in the old container — Docker only seeds a new named volume from the image, never from a running container's writable layer. If you have backups inside the container, rescue them before the first rebuild with the new compose file:

# 1. With the OLD container still running:
docker compose cp app:/var/www/html/backups ./backups-rescued   # old path

# 2. Now pull the new code + rebuild (this is what would have destroyed them):
git pull && docker compose up -d --build

# 3. Put them back on the new volume:
docker compose cp ./backups-rescued/. app:/var/www/backups
docker compose exec app chown -R www-data:www-data /var/www/backups

If docker compose cp reports the path does not exist, you had no on-container backups and there is nothing to migrate — just rebuild.

One-time step if you saved text-to-speech API keys on an older version

In v4.2.27 and earlier, the API keys you saved under Settings → Voice & Speech (Deepgram, an OpenAI-compatible server) were written to /var/www/html/keys/tts — inside the container's writable layer, so the update command above destroyed them (the engine then quietly fell back to Piper). They now live in /var/www/keys/tts, on the app_keys volume. If you have saved such a key and are still on the old container, rescue it before the first rebuild:

docker compose cp app:/var/www/html/keys/tts ./tts-keys-rescued   # old path
git pull && docker compose up -d --build
docker compose cp ./tts-keys-rescued/. app:/var/www/keys/tts
docker compose exec app chown -R www-data:www-data /var/www/keys/tts

If docker compose cp reports the path does not exist you never saved a key — nothing to do. If the container was already rebuilt, just paste the key again under Voice & Speech; it is stored in the right place now.

Getting a backup off the box

A volume survives rebuilds, but it still lives on this host. Copy the archive somewhere else (the 3-2-1 rule — see docs/BACKUP-RECOVERY-RUNBOOK.md):

docker compose cp app:/var/www/backups ./ticketscad-backups

…or use Settings → Backup / Maintenance → Download Full Backup, which streams the archive straight to the browser's machine.

Back up the database at any time:

docker compose exec db sh -c 'exec mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' > backup.sql

5. Upgrading

Schema migrations run automatically on container start — so an upgrade is just "get the newer code, rebuild the image, restart." Back up your database first (a migration changes the schema in place):

# 0. If your compose file predates the app_backups volume, do the one-time
#    rescue in §4 FIRST — step 3 below destroys anything not on a volume.

# 1. Back up first (see §4). This writes to the host, so it is safe either way.
docker compose exec db sh -c 'exec mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' > backup-$(date +%F).sql

#    (Or use TicketsCAD's own backup, which now lands on the app_backups volume
#     and survives the rebuild:  docker compose exec app php tools/backup_run.php --force)

# 2. Get the new version. For production, pin to a released tag (a known-good
#    version) rather than bleeding-edge main:
git fetch --tags
git checkout v4.0.1                       # newest tag: git tag -l | sort -V | tail -1
#   (or, to track the latest development: git pull)

# 3. Rebuild + restart. The --build is REQUIRED — a plain `git pull` alone does
#    NOT update the running container (it keeps the old built image).
docker compose up -d --build

How migrations are handled: the container entrypoint runs tools/install_fresh.php on every start. On an empty database it installs the full schema; on an existing one it applies only the pending migrations. Every migration is tracked in the _migrations table, so already-applied ones are skipped and nothing is dropped. It's idempotent and safe to re-run on every restart.

Confirm it worked: migrations are non-fatal by design (a failure never blocks the app from starting), so after any upgrade check the log:

docker compose logs app | grep -iE 'migration|install|error'

If an upgrade misbehaves, roll back to your backup and the previous tag:

docker compose exec -T db sh -c 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' < backup-YYYY-MM-DD.sql
git checkout v4.0.0 && docker compose up -d --build

6. Common operations

docker compose ps                 # container status
docker compose logs -f app        # follow app logs
docker compose logs -f db         # follow database logs
docker compose restart app        # restart just the app
docker compose down               # stop (data volumes preserved)
docker compose down -v            # stop AND DELETE all data volumes (destructive!)
docker compose exec app bash      # shell inside the app container

Run a health check from inside the container:

docker compose exec app php tools/health_check.php   # if present in your build

…or open the System Status (or Diagnostics) health page in the web UI.


7. Using your own config.php

By default the entrypoint generates config.php from the environment variables above. If you'd rather manage the full config yourself (extra settings, custom SMTP, etc.), create a config.php from config.example.php and mount it — the entrypoint will detect it and leave it alone. Uncomment this line in docker-compose.yml:

    volumes:
      - ./config.php:/var/www/html/config.php:ro

8. Production notes

The quick-start is plaintext HTTP on port 8081 — perfect for evaluation, not for an internet-facing deployment. For production:

  • Put it behind TLS. Run a reverse proxy (Caddy, nginx, or Traefik) that terminates HTTPS and forwards to the app container, and set NEWUI_BASE_URL to your https:// hostname. TicketsCAD enforces secure cookies when the base URL is https://.
  • Use strong, unique passwords for NEWUI_DB_PASS and DB_ROOT_PASSWORD.
  • Don't publish the database port. The compose file keeps MariaDB on the internal Docker network only — leave it that way.
  • Back up db_data + app_keys on a schedule (see §4).
  • Voice features are opt-in via a compose profile — see §8a. The hardware radio bridges (native DMR/AMBE to BrandMeister, Meshtastic) need a physical radio/serial device and run on the host, not in this stack — see their docs under docs/ (RADIO-DMR-INSTALL.md, Meshtastic guide).

8a. Voice features (Zello + DMR push-to-talk)

The browser radio widget's push-to-talk needs a small WebSocket relay per network (Zello, DMR). These ship as an optional compose profile that reuses the app image (nothing extra to build) and are off by default:

docker compose --profile voice up -d          # app + db + zello-proxy + dmr-proxy

That's it — the app image is already wired to reverse-proxy the browser's wss://<host>/zello-ws and wss://<host>/dmr-ws connections to the two relay containers (zello-proxy on 8090, dmr-proxy on 8092). You do not publish those ports; they stay on the internal Docker network.

Then, in the app, enable and configure the channels:

  1. Log in as an admin → Settings → Communications & Integrations (Zello and/or DMR).
  2. Enter your Zello credentials (Work network + username/password, or Consumer token) and/or your DMR bridge channels. Leave the proxy ports at their defaults (8090 / 8092) so the built-in WebSocket routing matches.
  3. Open the radio widget on the dashboard — it connects through the app to the relay. Green status = connected.

Notes:

  • Behind your own TLS reverse proxy (Caddy/nginx/Traefik, §8): make sure it WebSocket-upgrades /zello-ws and /dmr-ws in addition to normal traffic, or push-to-talk won't connect.
  • Turn it off by bringing the stack up without the profile again (docker compose up -d) — the relays stop; the core CAD is unaffected.
  • The relays read their settings from the same database; they carry no state of their own.
  • This profile covers Zello and the DMR relay. The native DMR/AMBE bridge to BrandMeister and Meshtastic need real radio hardware on the host and are deployed separately (see their docs).

9. Troubleshooting

Symptom Likely cause / fix
dependency failed to start: container ticketscad_db is unhealthy The database container exited before it became healthy — the app then refuses to start. Get the real reason with docker compose logs db. See "Database won't start on a small host" below — on Raspberry Pi and low-RAM VMs this is almost always out-of-memory or a 32-bit OS.
App log stuck on "Waiting for database" DB still initializing on first boot — wait; docker compose logs db for errors.
Container is up but no TicketsCAD in the browser You're at the wrong address. The app is at http://localhost:8081 (the port after the colon) — not :8080, and not a folder path like /ticketscad or /ticketscad_newui (those are the container names, not URLs). A plain http://localhost showing "Apache2 Ubuntu Default Page" / "It works!" is your host's own web server, not TicketsCAD — ignore it and use :8081. New to this? See GETTING-STARTED-FOR-BEGINNERS.md.
"database not reachable after 120s" Wrong NEWUI_DB_PASS vs. what the DB was first created with. If you changed it after first boot, the db_data volume still has the old one — docker compose down -v to reset (destroys data) or fix the password to match.
Login page loads but assets are 404 .htaccess disabled — the image enables AllowOverride All; if you customized Apache, restore it.
Forgot the generated admin password docker compose exec app php tools/create_admin.php --username=admin --email=you@example.com prints a fresh temp password (first login forces a change).
Want a clean slate docker compose down -v && docker compose up -d --build (deletes ALL data).
docker compose pull fails with pull access denied for ticketscad-newui (or repository does not exist) Don't run docker compose pull for this project — the app image (ticketscad-newui:local) is built locally, on your own machine, from this repo's Dockerfile; it is never published to Docker Hub or any registry, so there's nothing there to pull. This is expected, not a sign anything is broken. To get the latest version, use git pull then docker compose up -d --build (see §5) — that's the whole update, no separate pull step. If a plain docker compose up -d (no --build, no prior pull) won't bring your containers back up either, see §5 and the troubleshooting rows above for the real error from docker compose logs.

Database won't start on a small host (Raspberry Pi, low-RAM VMs)

If docker compose up ends with dependency failed to start: container ticketscad_db is unhealthy, the MariaDB container exited before it finished starting. Always look at the real reason first:

docker compose logs db

On a Raspberry Pi or a small VM, the cause is almost always one of these:

  1. Out of memory. MariaDB gets OOM-killed — often while the app image is still building in parallel (that build is CPU/RAM heavy and can take 15+ minutes on a Pi). The db log ends abruptly or the host shows an OOM message.

    • Check free memory: free -h.
    • Give the Pi swap if it has little RAM (a 2 GB swapfile is plenty), or build the image and start the database in two steps so they don't compete: docker compose build first, then docker compose up -d.
    • 1 GB Pis are tight; 2 GB or more is comfortable.
  2. 32-bit operating system. MariaDB 11 only ships 64-bit images (arm64/amd64). On a Pi, run the 64-bit Raspberry Pi OS — uname -m should print aarch64, not armv7l/armhf.

  3. A half-initialized data volume from an earlier failed attempt. Start completely clean (this deletes any data, which is fine on a first install): docker compose down -v then docker compose up -d.

Once docker compose logs db shows mariadbd: ready for connections, the app container will start on its own.


Questions or problems? Open an issue at https://github.com/openises/TicketsCAD/issues.