| eyebrow | Docs · Recipes | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| lede | Laravel Sail with an OPC UA test server sidecar. Docker Compose, the simplest possible dev loop, and the few gotchas around socket paths in containerised environments. | |||||||||||||||
| see_also |
|
|||||||||||||||
| prev |
|
|||||||||||||||
| next |
|
Laravel Sail is the lightest local-development environment for Laravel. Add an OPC UA test server as a sidecar and you have an end-to-end dev environment in two Docker images.
In docker-compose.yml, add a service:
services:
laravel.test:
# ... existing Sail config
opcua-test:
image: ghcr.io/php-opcua/uanetstandard-test-suite:latest
ports:
- "${OPCUA_PORT:-4840}:4840" # unsecured
- "${OPCUA_SECURE_PORT:-4841}:4841" # secured
networks:
- sail
healthcheck:
test: ["CMD", "sh", "-c", "nc -z localhost 4840"]
interval: 5s
retries: 10
Bring it up:
./vendor/bin/sail up -dIn .env:
OPCUA_ENDPOINT=opc.tcp://opcua-test:4840Note: the hostname is opcua-test (the Docker service name),
not localhost — Sail networks containers together.
For the secured endpoint:
OPCUA_ENDPOINT=opc.tcp://opcua-test:4841
OPCUA_SECURITY_POLICY=Basic256Sha256
OPCUA_SECURITY_MODE=SignAndEncrypt
OPCUA_CLIENT_CERT=/var/www/html/dev-certs/client.pem
OPCUA_CLIENT_KEY=/var/www/html/dev-certs/client.keyGenerate dev certs:
mkdir -p dev-certs
./vendor/bin/sail exec laravel.test openssl req -x509 -newkey rsa:2048 \
-keyout dev-certs/client.key \
-out dev-certs/client.pem \
-days 365 -nodes \
-subj "/CN=Sail Dev/O=Local" \
-addext "subjectAltName=URI:urn:sail-dev:client"dev-certs/ is gitignored by default. Don't commit dev keys.
The daemon is just a long-running PHP process — it runs inside
the laravel.test container.
For interactive dev (start it when you need it):
./vendor/bin/sail artisan opcua:sessionFor backgrounded dev:
services:
opcua-daemon:
image: sail-8.4/app
extends: laravel.test
command: php /var/www/html/artisan opcua:session
depends_on:
opcua-test:
condition: service_healthy
volumes:
- .:/var/www/html
networks:
- sail
Now sail up brings up Laravel, the OPC UA test server, AND the
daemon. Three-container dev environment.
In .env (managed mode under Sail):
OPCUA_ENDPOINT=opc.tcp://opcua-test:4840
OPCUA_SESSION_MANAGER_ENABLED=true
OPCUA_SOCKET_PATH=/var/www/html/storage/framework/opcua-session-manager.sockBoth laravel.test and opcua-daemon see the same socket file
via the shared . volume.
./vendor/bin/sail artisan tinker
> Opcua::isSessionManagerRunning()
=> true
> Opcua::read('i=2256')->value
=> 0i=2256 is the standard Server_ServerStatus_State node — value
0 means "Running". If you see this, the dev stack is wired.
The trust store lives at storage/app/opcua/trust/. Under Sail,
that's inside the laravel.test container but mapped to your
host:
# laravel-opcua does not ship opcua:trust:add — use opcua-cli.
./vendor/bin/sail bash -lc \
'OPCUA_TRUST_STORE_PATH=storage/app/opcua/trust \
vendor/bin/opcua-cli trust:add opc.tcp://opcua-test:4841'The cert lands in storage/app/opcua/trust/<hash>.pem. Both your
host and the Sail container see it — convenient for editing
during dev.
For dev work on subscription / event flow:
OPCUA_AUTO_PUBLISH=trueRestart the daemon:
./vendor/bin/sail restart opcua-daemonNow Opcua::subscribe() fires Laravel events that your dev
listeners receive.
Run tests as usual:
./vendor/bin/sail test # everything except Integration
./vendor/bin/sail test tests/Integration # Integration with the live test serverThe integration tests target opcua-test automatically because
that's what .env points at.
For broadcasting dev:
services:
reverb:
image: sail-8.4/app
extends: laravel.test
command: php /var/www/html/artisan reverb:start
ports:
- "${REVERB_PORT:-8080}:8080"
volumes:
- .:/var/www/html
networks:
- sail
REVERB_HOST=localhost # browser-visible
VITE_REVERB_HOST=localhost
REVERB_PORT=8080Run:
./vendor/bin/sail up -d
./vendor/bin/sail npm run dev # ViteBrowse http://localhost. Real-time tag updates appear in the
browser as the test server's CurrentTime ticks (or whatever
tags you subscribe to).
PHP doesn't hot-reload. After editing daemon-touching code, kill and restart:
./vendor/bin/sail restart opcua-daemonFor listener changes, you don't need to restart the daemon — listener resolution happens per-event from the application's container, which gets re-bootstrapped each time.
./vendor/bin/sail down -v # -v drops volumes tooFor a clean slate including the trust store:
./vendor/bin/sail down -v
rm -rf storage/app/opcua/trustThe same docker-compose.yml runs in CI. The CI workflow:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d
- run: docker compose exec -T laravel.test composer install
- run: docker compose exec -T laravel.test php artisan test
- run: docker compose down
Local dev and CI use the same fixtures — surprises are rare.
| Image | When |
|---|---|
ghcr.io/php-opcua/uanetstandard-test-suite:latest |
Full coverage (8 endpoints) |
ghcr.io/php-opcua/extra-test-suite:latest |
open62541-backed, supports method calls |
mcr.microsoft.com/iotedge/opc-plc:latest |
Microsoft's open-source PLC simulator |
For most dev, the uanetstandard-test-suite is the right choice
— it exercises every OPC UA feature the package supports.
- Production deployment — same patterns, real iron.
- Integration tests — the full test-suite story.