Beacon ships as a Compose bundle: docker-compose.yml, .env.example, and HTTP on port 8000. You do not need a GitHub checkout or auth.json. A naked docker run of the GHCR image is not enough — that image is PHP-FPM only (no published HTTP port, no Nginx, no Redis, no Horizon).
Current image: ghcr.io/apxcde/beacon:0.3.3-beta. Override with BEACON_IMAGE in .env.
What docker compose up starts
app— the Laravel PHP-FPM application container (ghcr.io/apxcde/beacon)nginx— the public HTTP entrypoint onhttp://localhost:8000(override withBEACON_PORT)redis— queue backend for Horizonhorizon— queue worker and Horizon supervisorscheduler— Laravel scheduler loopmysql— optional MySQL service, enabled only when you start themysqlprofile
The Docker setup uses SQLite by default so a fresh install does not need a separate database container. If you prefer MySQL, switch the database environment variables and enable the MySQL profile.
Inside Docker, the app defaults to /var/www/html/storage/app/database/beacon.sqlite when SQLite is enabled. That container-only path is intentionally kept out of Laravel's DB_DATABASE in .env.example (that value is for php artisan serve).
First boot
Download the bundle from your license portal, or curl the same files:
mkdir beacon && cd beacon
curl -fsSL https://apexcode.dev/beacon/docker-compose.yml -o docker-compose.yml
curl -fsSL https://apexcode.dev/beacon/env.example -o .env.example
cp .env.example .env
echo '<token-from-your-license-portal>' | docker login ghcr.io -u apxcde-distrib --password-stdin
docker compose up -d
docker compose cp ./beacon.license app:/var/www/html/storage/app/beacon.license
Beacon is at http://localhost:8000.
If you installed via Composer (composer create-project apxcde/beacon from packages.apexcode.dev), skip the curls — docker-compose.yml and .env.example are already in the project. Copy beacon.license to storage/app/beacon.license, then cp .env.example .env and docker compose up -d from that directory.
On first boot the app container will:
- create a persistent app key if one is not already set
- create the SQLite database file under
storage/app/database/beacon.sqlitewhen SQLite is enabled - wait for MySQL when
DB_CONNECTION=mysql - run
php artisan migrate --force - create the public storage symlink if needed
Database modes
SQLite default
No extra services are required:
docker compose up -d
The default compose environment points Laravel at:
DB_CONNECTION=sqliteDB_DATABASE=/var/www/html/storage/app/database/beacon.sqlite(set by the container start script)
MySQL profile
Start Beacon with the MySQL profile and override the Laravel database connection:
DB_CONNECTION=mysql \
MYSQL_DATABASE=beacon \
MYSQL_USER=beacon \
MYSQL_PASSWORD=secret \
MYSQL_ROOT_PASSWORD=root \
docker compose --profile mysql up -d
If you only set DB_CONNECTION=mysql, the container startup script defaults DB_DATABASE to MYSQL_DATABASE so first boot still targets a valid MySQL schema name.
The PHP container already includes both pdo_sqlite and pdo_mysql, so switching between SQLite and MySQL is just an environment change.
The bundled MySQL container is internal to the compose network by default and does not publish port 3306 to your host, which avoids conflicts with any local MySQL instance you may already be running.
Validate the install
- Open
http://localhost:8000 - Complete the setup dashboard:
- create the default workspace
- save Twilio credentials
- register a primary Twilio number
- generate an API key
- Confirm the health endpoint responds:
curl http://localhost:8000/up
- Confirm the OpenAPI spec is reachable:
curl http://localhost:8000/openapi.yaml
- Send a test API request using the generated key:
curl -X POST http://localhost:8000/api/text \
-H "X-API-Key: beacon_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+15551234567",
"message": "Hello from Beacon"
}'
- Connect an MCP client:
claude mcp add beacon http://localhost:8000/api/mcp \
--transport http \
--scope user \
--header "X-API-Key: beacon_your_key_here"
Useful commands
# stop the stack
docker compose down
# stop the stack and remove named volumes (including the image-copied code volume)
docker compose down -v
# inspect the PHP-FPM app logs
docker compose logs -f app
# inspect Nginx logs
docker compose logs -f nginx
# inspect Horizon logs
docker compose logs -f horizon
Notes
- The MCP endpoint is served by the Laravel app at
/api/mcp; there is no separate MCP container. - Persistent app state lives in the
beacon_storageandbeacon_redisnamed volumes. beacon_codeis populated from the image on first boot so Nginx can serve/publicwithout a local checkout. Afterdocker compose pull, remove that volume before up so hashed frontend assets match the new image:docker compose down && docker volume rm beacon_beacon_code && docker compose up -d.- If you want to inject your own app key instead of using the generated one, set
APP_KEYin.envbefore startup. Compose interpolates that value into the container environment; variables that are only in.envand not referenced indocker-compose.ymlnever reach Laravel. - Override the published port with
BEACON_PORTand setAPP_URLto match (for exampleBEACON_PORT=8080andAPP_URL=http://localhost:8080). - Image updates during your update window land as new tags on
ghcr.io/apxcde/beaconand as Composer packages onpackages.apexcode.dev. See licensing.md.