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.
Queue workers and Horizon
Section titled “Queue workers and Horizon”Marketix uses Laravel Horizon as its Redis-backed queue manager.
The horizon service
Section titled “The horizon service”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
appcontainer is healthy (ensuring migrations have run). - It shares the same image and environment variables as
app. SIGTERMtriggers Horizon’s graceful shutdown, allowing in-flight jobs to finish before the process exits.
Horizon is configured in config/horizon.php. Key defaults:
| Setting | Value | Notes |
|---|---|---|
| Queue connection | redis (default connection) | Configured via REDIS_* env vars. |
| Max processes (production) | 10 | Auto-scaling with balance=auto. |
| Max processes (local) | 3 | |
| Job timeout | 60 seconds | Per-job limit. |
| Memory limit (master) | 64 MB | Master supervisor restarts if exceeded. |
| Job retention (recent/completed) | 60 minutes | |
| Job retention (failed) | 7 days (10 080 minutes) |
Horizon dashboard
Section titled “Horizon dashboard”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.
Manual queue management
Section titled “Manual queue management”# Gracefully terminate Horizon (it will restart automatically via Docker restart policy)docker compose exec horizon php artisan horizon:terminate
# Check Horizon statusdocker 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:clearScheduler
Section titled “Scheduler”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"]Scheduled tasks
Section titled “Scheduled tasks”The following tasks are registered in routes/console.php:
| Command | Frequency | Purpose |
|---|---|---|
marketix:geoip:update | Daily | Download and install the MaxMind GeoLite2-City database. |
activitylog:clean | Daily | Delete activity log entries older than 365 days. |
domains:check-status (closure) | Every 15 minutes | Dispatch CheckDomainStatusJob for each domain to verify reachability and update Traefik config. |
GeoIP database
Section titled “GeoIP database”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.
Prerequisites
Section titled “Prerequisites”You need a free MaxMind account and a license key. Set it in .env:
MAXMIND_LICENSE_KEY=your_key_hereThis is read from config/services.php (services.maxmind.license_key).
Updating the database manually
Section titled “Updating the database manually”docker compose exec app php artisan marketix:geoip:updateThe 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).
Testing GeoIP lookups
Section titled “Testing GeoIP lookups”docker compose exec app php artisan marketix:geoip:test 8.8.8.8The 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).
Activity log
Section titled “Activity log”The global activity log at /admin/activity shows a unified audit trail of all events across every project on the instance.
Log names (event types)
Section titled “Log names (event types)”| Log name | Events captured |
|---|---|
url | Short link create / update / delete |
domain | Domain create / update / delete |
qrcode | QR code create / update / delete |
pixel | Pixel create / update / delete |
project | Project create / update / delete |
membership | Member role changes |
invitation | Invitation sent / revoked |
security | 2FA enable/disable, password changes |
Filtering
Section titled “Filtering”The activity log supports the following filters, applied via query parameters:
| Filter | Description |
|---|---|
| Log name | Narrow to a single event type from the list above. |
| Project | Narrow to events within a specific project. |
| Causer | Search by user name or email. |
| From / To | Date range filter. |
Filters update the URL so you can bookmark or share a filtered view.
Retention
Section titled “Retention”Activity log entries are retained for 365 days. The activitylog:clean command (scheduled daily) deletes entries older than this threshold.
Upgrades
Section titled “Upgrades”Marketix images are published to noixdev/marketix:latest. The Docker Compose stack includes Watchtower to automate upgrades.
Automated upgrades with Watchtower
Section titled “Automated upgrades with Watchtower”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: 300WATCHTOWER_ROLLING_RESTART: "true" means containers are updated one at a time, reducing downtime.
Manual upgrade
Section titled “Manual upgrade”docker compose pull app horizon schedulerdocker compose up -d app horizon schedulerThe 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.
Rollback
Section titled “Rollback”If an upgrade introduces a regression, roll back by pinning the image to a previous tag:
# Pin to a specific digest or tag in docker-compose.yml, then:docker compose up -d app horizon schedulerTroubleshooting
Section titled “Troubleshooting”Jobs not processing
Section titled “Jobs not processing”- Check the Horizon container is running:
docker compose ps. - Check Horizon logs:
docker compose logs horizon. - Verify Redis is reachable:
docker compose exec horizon php artisan tinker --execute="Redis::ping()". - Check the Horizon dashboard at
/horizonfor failed jobs.
Scheduled tasks not running
Section titled “Scheduled tasks not running”- Check the scheduler container is running:
docker compose ps scheduler. - Check scheduler logs:
docker compose logs scheduler. - Run a task manually to test:
docker compose exec app php artisan marketix:geoip:update.
Custom-domain reachability
Section titled “Custom-domain reachability”When a project domain fails its reachability check:
- Verify that DNS for the custom domain points to the host running Marketix.
- Confirm the
/.well-known/marketixpath is reachable on the custom domain:This public route is registered without middleware and confirms that Traefik is routing the domain to the application correctly.Terminal window curl https://your-custom-domain.com/.well-known/marketix# Expected: {"app":"marketix"} - If the domain is newly added, Traefik’s file provider watches
custom-domains.ymlfor changes (thetraefik_dynamicvolume is shared betweenappandtraefik). Check that the file was written:docker compose exec traefik cat /traefik/custom-domains.yml. - 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.
Database or Redis not accessible
Section titled “Database or Redis not accessible”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:
docker compose exec db mariadb -u db -pdb dbdocker compose exec redis redis-cli -a "${REDIS_PASSWORD}" ping