Docker deployment
Marketix ships a production-ready docker-compose.yml that runs all required services in a single command. This page explains each service, the network topology, and how to wire custom short-link domains into the Traefik edge router.
Prerequisites
Section titled “Prerequisites”- Docker Engine 24+ and the Compose v2 plugin (
docker compose) - A domain pointed at your server’s public IP (for
APP_DOMAINand TLS) - An
.envfile on the host with the variables described in Environment configuration
Services
Section titled “Services”The Compose file defines seven services across two Docker networks.
traefik
Section titled “traefik”Image: traefik:v3.3
The edge router. It handles:
- HTTP→HTTPS redirect — all port 80 traffic is permanently redirected to port 443.
- TLS termination — certificates are issued automatically via Let’s Encrypt using the TLS-ALPN-01 challenge. The ACME email address comes from
LETSENCRYPT_EMAIL. - Docker provider — Traefik watches the Docker socket and picks up Marketix’s routing labels from the
appcontainer. - File provider — Traefik watches the
traefik_dynamicvolume for a YAML file (custom-domains.yml) written by the app when project custom domains are added or changed. This is how short-link custom domains get their TLS certificates without a Traefik restart.
Traefik mounts:
/var/run/docker.sock(read-only) for the Docker providertraefik_acmevolume at/acmeto persist the ACME account and issued certificatestraefik_dynamicvolume at/traefik(read-only) for the file provider
Image: noixdev/marketix:latest
The main web application. Runs Laravel Octane on FrankenPHP on port 8000. The entrypoint (docker/entrypoint.sh) waits for the database to become reachable, then — when RUN_MIGRATIONS=true — caches config and views and runs php artisan migrate --force before starting Octane.
The app container:
- Mounts the
storagevolume at/app/storageto persist uploaded files, generated PDFs, QR code assets, and logs. - Mounts the
traefik_dynamicvolume at/traefik(read-write) so theRegenerateTraefikConfigJobjob can writecustom-domains.ymlwhenever project domains change. - Exposes itself to Traefik via Docker labels:
labels: - traefik.enable=true - traefik.docker.network=web - traefik.http.routers.marketix.rule=Host(`${APP_DOMAIN}`) - traefik.http.routers.marketix.entrypoints=websecure - traefik.http.routers.marketix.tls.certresolver=letsencrypt - traefik.http.services.marketix.loadbalancer.server.port=8000APP_DOMAIN is your primary application hostname (e.g. app.example.com). The admin panel and the project dashboard are all served from this domain.
horizon
Section titled “horizon”Image: noixdev/marketix:latest
Runs php artisan horizon to process queued jobs. Horizon uses Redis as its queue backend (QUEUE_CONNECTION=redis).
Jobs that run through the queue include:
- Regenerating the Traefik custom-domain config file when domains are added, edited, or removed.
- Any other background work dispatched by the application.
Horizon waits for the app container to become healthy (which means migrations have already run) before starting.
scheduler
Section titled “scheduler”Image: noixdev/marketix:latest
Runs php artisan schedule:work, keeping the Laravel scheduler alive. This handles any periodic tasks registered in the application (GeoIP database expiry checks, cleanup jobs, and similar cron-based work).
Image: mariadb:11.8
MariaDB database server. Data is persisted in the dbdata volume. The root password is set from MYSQL_ROOT_PASSWORD. The application user, password, and database are all set to db inside the Compose file.
The service exposes a healthcheck (healthcheck.sh --connect --innodb_initialized) so that the app and horizon containers wait for the database to be fully ready before starting.
Image: redis:7-alpine
Redis server with append-only persistence and password authentication. The horizon and app containers connect using the REDIS_PASSWORD from your .env.
watchtower
Section titled “watchtower”Image: ghcr.io/nicholas-fedor/watchtower:latest
Automatically pulls and redeploys updated images every five minutes. Only containers labelled com.centurylinklabs.watchtower.enable=true are watched — these are app, horizon, and scheduler. The stateful traefik, db, and redis containers are deliberately excluded.
Networks
Section titled “Networks”| Network | Purpose |
|---|---|
web | Shared between Traefik and the app container. Traefik routes inbound traffic to app over this network. |
marketix_internal | Backend network for app, horizon, scheduler, db, and redis. Not exposed to the host. |
Volumes
Section titled “Volumes”| Volume | Contents |
|---|---|
storage | Laravel storage directory: uploads, logs, generated PDFs, QR code assets |
dbdata | MariaDB data directory |
redisdata | Redis append-only log |
traefik_acme | ACME account and Let’s Encrypt certificates |
traefik_dynamic | Dynamically generated Traefik file-provider config (custom-domains.yml) |
Custom short-link domains
Section titled “Custom short-link domains”When a project member adds a custom domain in the Marketix UI, the app dispatches a job (picked up by Horizon) that regenerates custom-domains.yml and writes it to the traefik_dynamic volume. Because Traefik’s file provider has watch=true, it reloads the config live — no restart required.
The generated file follows the same structure as the included custom-domains.yml example:
http: routers: custom-<slug>: rule: Host(`short.example.com`) entrypoints: - websecure service: laravel-app tls: certResolver: letsencrypt services: laravel-app: loadBalancer: servers: - url: 'http://app:8000'Before adding a custom domain in the UI, the app verifies reachability by fetching /.well-known/marketix on that domain. This endpoint returns {"app":"marketix"} and confirms that the domain’s DNS is pointed at your server and that the Traefik routing is resolving to the correct application instance.
Starting the stack
Section titled “Starting the stack”- Copy
.env.exampleto.envand fill in the required values (see Environment configuration). - Pull the latest images:
docker compose pull- Start the stack:
docker compose up -d- Follow the startup logs to confirm the app container completes its health check:
docker compose logs -f appYou should see the Octane server start after migrations complete. Once the app container is healthy, horizon and scheduler will start automatically.
Continue to First run to create the first admin user and set up the GeoIP database.