Skip to content

Operations

This page covers the day-to-day operational concerns for a running Marketix instance: queue processing, scheduled tasks, GeoIP database maintenance, the global activity log, upgrades, and common troubleshooting steps.

Marketix uses Laravel Horizon as its Redis-backed queue manager.

In the Docker Compose stack, a dedicated horizon container runs alongside the main app container:

horizon:
image: noixdev/marketix:latest
command: ["php", "artisan", "horizon"]
stop_signal: SIGTERM
  • The container starts only after the app container is healthy (ensuring migrations have run).
  • It shares the same image and environment variables as app.
  • SIGTERM triggers Horizon’s graceful shutdown, allowing in-flight jobs to finish before the process exits.

Horizon is configured in config/horizon.php. Key defaults:

SettingValueNotes
Queue connectionredis (default connection)Configured via REDIS_* env vars.
Max processes (production)10Auto-scaling with balance=auto.
Max processes (local)3
Job timeout60 secondsPer-job limit.
Memory limit (master)64 MBMaster supervisor restarts if exceeded.
Job retention (recent/completed)60 minutes
Job retention (failed)7 days (10 080 minutes)

The Horizon UI is available at the path configured by HORIZON_PATH (default: horizon), optionally on a subdomain configured by HORIZON_DOMAIN. By default it is at https://<APP_DOMAIN>/horizon.

Terminal window
# Gracefully terminate Horizon (it will restart automatically via Docker restart policy)
docker compose exec horizon php artisan horizon:terminate
# Check Horizon status
docker compose exec horizon php artisan horizon:status
# Delete all pending jobs from the queue (does not retry — use queue:retry to retry failed jobs)
docker compose exec horizon php artisan horizon:clear

A dedicated scheduler container runs php artisan schedule:work, which executes the scheduler loop continuously (equivalent to a cron calling schedule:run every minute, without needing an actual crontab).

scheduler:
image: noixdev/marketix:latest
command: ["php", "artisan", "schedule:work"]

The following tasks are registered in routes/console.php:

CommandFrequencyPurpose
marketix:geoip:updateDailyDownload and install the MaxMind GeoLite2-City database.
activitylog:cleanDailyDelete activity log entries older than 365 days.
domains:check-status (closure)Every 15 minutesDispatch CheckDomainStatusJob for each domain to verify reachability and update Traefik config.

Marketix uses the MaxMind GeoLite2-City database to resolve visitor IP addresses to country and city for link statistics. The database must be present at storage/app/geoip/GeoLite2-City.mmdb for geo lookups to work.

You need a free MaxMind account and a license key. Set it in .env:

MAXMIND_LICENSE_KEY=your_key_here

This is read from config/services.php (services.maxmind.license_key).

Terminal window
docker compose exec app php artisan marketix:geoip:update

The command downloads the GeoLite2-City tarball (~60 MB) from MaxMind, extracts it, and places GeoLite2-City.mmdb at storage/app/geoip/GeoLite2-City.mmdb. The memory limit is raised to 512 MB for this command only to handle the large archive.

The scheduler runs this command daily. You should also run it manually after first deployment (see First run).

Terminal window
docker compose exec app php artisan marketix:geoip:test 8.8.8.8

The command validates the IP address, checks that the database file is present, performs a lookup, and prints a table of the results (country, city, latitude, longitude, etc.). It warns if no data is found, and notes if only country-level data is available (common for datacenter and VPN IPs in the free GeoLite2 dataset).

The global activity log at /admin/activity shows a unified audit trail of all events across every project on the instance.

Log nameEvents captured
urlShort link create / update / delete
domainDomain create / update / delete
qrcodeQR code create / update / delete
pixelPixel create / update / delete
projectProject create / update / delete
membershipMember role changes
invitationInvitation sent / revoked
security2FA enable/disable, password changes

The activity log supports the following filters, applied via query parameters:

FilterDescription
Log nameNarrow to a single event type from the list above.
ProjectNarrow to events within a specific project.
CauserSearch by user name or email.
From / ToDate range filter.

Filters update the URL so you can bookmark or share a filtered view.

Activity log entries are retained for 365 days. The activitylog:clean command (scheduled daily) deletes entries older than this threshold.

Marketix images are published to noixdev/marketix:latest. The Docker Compose stack includes Watchtower to automate upgrades.

The watchtower service polls Docker Hub every 5 minutes and redeploys any container tagged com.centurylinklabs.watchtower.enable=true when a new image is available. The app, horizon, and scheduler containers carry this label; traefik, db, and redis do not.

watchtower:
image: ghcr.io/nicholas-fedor/watchtower:latest
environment:
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_ROLLING_RESTART: "true"
WATCHTOWER_POLL_INTERVAL: 300

WATCHTOWER_ROLLING_RESTART: "true" means containers are updated one at a time, reducing downtime.

Terminal window
docker compose pull app horizon scheduler
docker compose up -d app horizon scheduler

The app container entrypoint runs database migrations automatically on startup (RUN_MIGRATIONS=true). The horizon and scheduler containers wait for the app container to be healthy before starting, ensuring migrations complete before workers process jobs.

If an upgrade introduces a regression, roll back by pinning the image to a previous tag:

Terminal window
# Pin to a specific digest or tag in docker-compose.yml, then:
docker compose up -d app horizon scheduler
  1. Check the Horizon container is running: docker compose ps.
  2. Check Horizon logs: docker compose logs horizon.
  3. Verify Redis is reachable: docker compose exec horizon php artisan tinker --execute="Redis::ping()".
  4. Check the Horizon dashboard at /horizon for failed jobs.
  1. Check the scheduler container is running: docker compose ps scheduler.
  2. Check scheduler logs: docker compose logs scheduler.
  3. Run a task manually to test: docker compose exec app php artisan marketix:geoip:update.

When a project domain fails its reachability check:

  1. Verify that DNS for the custom domain points to the host running Marketix.
  2. Confirm the /.well-known/marketix path is reachable on the custom domain:
    Terminal window
    curl https://your-custom-domain.com/.well-known/marketix
    # Expected: {"app":"marketix"}
    This public route is registered without middleware and confirms that Traefik is routing the domain to the application correctly.
  3. If the domain is newly added, Traefik’s file provider watches custom-domains.yml for changes (the traefik_dynamic volume is shared between app and traefik). Check that the file was written: docker compose exec traefik cat /traefik/custom-domains.yml.
  4. Let’s Encrypt certificate issuance can take up to a minute after the domain is first routed correctly. Check Traefik logs for ACME errors: docker compose logs traefik.

Both db and redis are on the marketix_internal network and are not published to the host by default. Access them via docker compose exec:

Terminal window
docker compose exec db mariadb -u db -pdb db
docker compose exec redis redis-cli -a "${REDIS_PASSWORD}" ping