Iremos usar o docker para fazer a instalação;
mkdir cursolaravel
cd cursolaravel
O Composer é um gerenciador de dependências para PHP. Ele permite instalar, atualizar e gerenciar bibliotecas e pacotes de forma simples, garantindo que um projeto tenha todas as dependências necessárias. No Laravel, o Composer é usado para instalar o framework e suas bibliotecas.
docker run --rm -it \
-v $(pwd):/app \
-u $(id -u):$(id -g) \
composer:latest \
composer create-project laravel/laravel .
Dockerfile pronto para usar no contexto USP:
FROM uspdev/uspdev-php-apache:latest
RUN sed -i 's|/var/www/html|/var/www/html/public|' \
/etc/apache2/sites-available/000-default.conf
USER www-data
COPY --chown=www-data . .
RUN composer install \
--no-dev \
--optimize-autoloader \
--no-interaction
CMD ["apache2-foreground"]
docker-compose.yml pronto para usar no contexto USP:
services:
cursolaravel:
build: .
image: cursolaravel:latest
container_name: cursolaravel
ports:
- "8000:80"
depends_on:
- mariadb
networks:
- cursolaravel-network
volumes:
- ./:/var/www/html
environment:
HOME: /tmp
user: "${UID:-1000}:${GID:-1000}"
mariadb:
image: mariadb:11
container_name: cursolaravel_mariadb
restart: always
environment:
MYSQL_DATABASE: cursolaravel
MYSQL_USER: cursolaravel
MYSQL_PASSWORD: cursolaravel
MYSQL_ROOT_PASSWORD: cursolaravel
volumes:
- mariadb_data:/var/lib/mysql
networks:
- cursolaravel-network
phpmyadmin:
image: phpmyadmin:latest
container_name: cursolaravel_phpmyadmin
restart: always
depends_on:
- mariadb
ports:
- "8081:80"
environment:
PMA_HOST: mariadb
PMA_PORT: 3306
PMA_USER: root
PMA_PASSWORD: cursolaravel
networks:
- cursolaravel-network
selenium:
image: selenium/standalone-chrome
container_name: cursolaravel_selenium
ports:
- "7900:7900"
networks:
- cursolaravel-network
shm_size: 2gb
# No /etc/hosts do seu computador colocar a linha `127.0.0.1 auth.local`
# particularidades do protocolo oAuth1 usado na USP
senhaunica-faker:
image: uspdev/senhaunica-faker
container_name: cursolaravel_senhaunica-faker
ports:
- "3141:3141"
environment:
- APP_URL=http://auth.local:3141
networks:
cursolaravel-network:
aliases:
- auth.local
networks:
cursolaravel-network:
volumes:
mariadb_data:
Criando a imagem e subindo ambiente:
docker compose up --build
Acessar o laravel criado: http://127.0.0.1:8000/
Acessar phpmyadmin criado: http://127.0.0.1:8081/
Acessar servidor de autenticação USP: http://auth.local:3141
No arquivo .env vamos trocar para mariadb:
DB_CONNECTION=mariadb
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=cursolaravel
DB_USERNAME=cursolaravel
DB_PASSWORD=cursolaravel
Recriando tabelas no banco de dados:
docker exec -it cursolaravel php artisan migrate
Uma rota é a forma como o framework define e gerencia URLs para acessar diferentes partes da aplicação. As rotas são configuradas no arquivo routes/web.php (para páginas web) ou routes/api.php (para APIs) e determinam qual código será executado quando um usuário acessa uma URL específica. Exemplo:
Route::get('/rota-sem-controller', function () {
echo "Uma rota sem controller, not good!";
});
O controller é uma classe responsável por organizar a lógica da aplicação, separando as regras de negócio das rotas. Em vez de definir toda a lógica diretamente nas rotas, os controllers agrupam funcionalidades relacionadas, tornando o código mais limpo e modular.
A convenção de nomenclatura para controllers segue o padrão PascalCase, onde o nome deve ser descritivo, no singular e sempre terminar com "Controller", como ProdutoController ou UsuarioController.
docker exec -it cursolaravel php artisan make:controller MeuPrimeiroController
Método do controller:
public function index(){
return 'Uma rota com controller, Great!';
}
Mesma ideia, mas com controller:
use App\Http\Controllers\MeuPrimeiroController;
Route::get('/rota-com-controller', [MeuPrimeiroController::class,'index']);
Com essa ideia, vamos criar um sistema de cadastro de livros:
docker exec -it cursolaravel php artisan make:controller LivroController
A seguir criamos a rota livros e a apontamos para o controller LivroController, importando anteriormente o namespace App\Http\Controllers\LivroController.
use App\Http\Controllers\LivroController;
Route::get('/livros', [LivroController::class,'index']);
A camada View é responsável por exibir a interface da aplicação, separando a lógica de apresentação da lógica de negócio (controller). Ela utiliza o Blade, uma linguagem de templates que permite criar páginas dinâmicas de forma eficiente. As views ficam armazenadas na pasta resources/views e podem ser retornadas a partir de um controller usando return view('nome_da_view').
mkdir resources/views/livros
touch resources/views/livros/index.blade.php
No controller:
public function index(){
return view('livros.index');
}
Conteúdo mínimo de index.blade.php:
<!DOCTYPE html>
<html>
<head>
<title>Livros</title>
</head>
<body>
Memórias de um Sargento de Milícias<br>
O Primo Basílio<br>
Memórias Póstumas de Brás Cubas<br>
A Hora da Estrela<br>
</body>
</html>
O Model é uma representação de uma tabela no banco de dados e é responsável pela interação com os dados dessa tabela.
Criando o model chamado Livro:
docker exec -it cursolaravel php artisan make:model Livro -m
As migrations são uma forma de versionar e gerenciar o esquema do banco de dados, permitindo criar, alterar e remover tabelas de forma controlada e rastreável.
Cada migration é uma classe PHP que define as operações a serem realizadas no banco de dados. As migrations são armazenadas na pasta database/migrations. Vamos colocar três colunas para o model Livro: titulo, autor e ano.
$table->string('titulo');
$table->string('autor');
$table->integer('ano');
Depois da modificação na migration, aplicá-la no banco de dados: docker exec -it cursolaravel php artisan migrate.
O comando docker exec -it cursolaravel php artisan tinker nos permite digitar comandos PHP e ver imediatamente o resultado, como se estivesse dentro da sua aplicação Laravel, ou seja, executamos comandos PHP diretamente dentro do contexto da aplicação, de forma prática e rápida.
Usando o tinker, vamos cadastrar dois livros:
$livro = new \App\Models\Livro;
$livro->titulo = "Memórias de um Sargento de Milícias";
$livro->autor = "Manuel Antônio de Almeida";
$livro->ano = 1853;
$livro->save();
$livro = new \App\Models\Livro;
$livro->titulo = "O Primo Basílio";
$livro->autor = "Eça de Queiroz";
$livro->ano = 1878;
$livro->save();
Na view da index podemos listar os livros cadastrados:
use App\Models\Livro;
public function index(){
return view('livros.index',[
'livros' => Livro::all()
]);
}
No blade index.blade.php, listamos os livros:
<h1>Listagem de Livros</h1>
<ul>
@foreach($livros as $livro)
<li>{{ $livro->titulo }}, por <i>{{ $livro->autor }}</i> em {{ $livro->ano }}</li>
@endforeach
</ul>
Inserindo um campo para busca simples no blade:
<form>
<input type="text" name="search" value="{{ request('search') }}">
<button type="submit">Pesquisar</button>
</form>
E no controller temos que tratar a busca:
public function index(Request $request){
if($request->has('search')){
$livros = Livro::where('titulo','like','%'.$request->search.'%')->get();
} else {
$livros = Livro::all();
}
return view('livros.index',[
'livros' => $livros
]);
}
Por fim, podemos criar um comando no artisan que automatiza o cadastro de livros a partir de alguma lógica que podemos desenvolver.
docker exec -it cursolaravel php artisan make:command ImportaLivros
O comando acima criará o arquivo app/Console/Commands/ImportaLivros.php, vamos implementar a inserção de um livro via linha de comando:
public function handle()
{
$livro = new \App\Models\Livro;
$livro->titulo = "A Hora da Estrela";
$livro->autor = "Clarice Lispector";
$livro->ano = 1977;
$livro->save();
}
Ao rodarmos no terminal o comando docker exec -it cursolaravel php artisan app:importa-livros o livro da Clarice será cadastrado. Essa é um implementação simples (e inútil, pois o mesmo livro é sempre cadastrado repetidamente), mas a ideia é que qualquer lógica pode ser implementada no handle() para cadastro de muitos livros a partir de uma fonte externa, como, por exemplo, uma lista de livros oriunda de um arquivo csv.
Os testes com Laravel Dusk no nosso contexto tem dois propósitos:
docker exec -it cursolaravel composer require --dev laravel/dusk
docker exec -it cursolaravel php artisan dusk:install
docker exec -it cursolaravel php artisan dusk:chrome-driver
Para rodar os testes, configure no .env (e aproveite coloque no .env.example):
APP_URL=http://cursolaravel
DUSK_DRIVER_URL='http://selenium:4444/wd/hub'
DUSK_START_MAXIMIZED=true
DUSK_HEADLESS_DISABLED=true
Criando uma classe do Dusk para inserirmos nosso teste:
docker exec -it cursolaravel php artisan dusk:make BuscaLivroTest
Criando um teste que verifica se na rota /livros existe a frase "Listagem de Livros":
$browser->visit('/livros')
->pause(2000)
->typeSlowly('search', 'primo', 300)
->pause(2000)
->press('Pesquisar')
->pause(2000)
->assertSee('O Primo Basílio');
Acessar http://localhost:7900/ com senha secret e assistir o teste rodando.
Rodar o teste:
docker exec -it cursolaravel php artisan dusk tests/Browser/BuscaLivroTest.php
1 - Criar um comando para importar os livros do arquivo csv livros no model Livro. Importante:
handle(), implemente a lógica para ler o arquivo livros.csv e para cada livro, fazer a inserção;\App\Models\Livro::truncate() no começo do método handle().League\Csv\Reader (disponível via Composer) para facilitar a leitura do CSV.2 - Criar teste Dusk para buscar a string "processo" e deverá ter um assert para ver Franz Kafka e um assert not para José de Alencar;
3 - Criar estatísticas básicas sobre os dados importados
EstatisticaController com um método chamado stats.livros/stats que aponte para o método stats.stats apresente uma tabela com a quantidade de livros por ano.Exemplo de saída (com dados fictícios):
| ano | quantidade |
|---|---|
| 1998 | 4 |
| 2000 | 7 |
4 - No método stats apresente uma segunda tabela com a quantidade de livros por autor.
Na próxima reunião, cada membro do grupo (estagiários e funcionários) deve apresentar na TV rapidamente e solução do exercício.
CRUD é um acrônimo para as quatro operações básicas utilizadas na manipulação de dados em sistemas web: Create (Criar), Read (Ler), Update (Atualizar) e Delete (Excluir). Essas operações interagem com bancos de dados, permitindo, por exemplo, que usuários possam cadastrar novas informações, visualizar registros existentes, modificar dados já salvos e remover registros.
São geralmente necessárias duas rotas para salvar um registro em uma operação CRUD porque o processo é dividido em duas etapas: exibir o formulário e processar os dados enviados. A rota GET serve para exibir o formulário de criação e a rota POST serve para processar os dados enviados pelo formulário no controller:
Route::get('/livros/create', [LivroController::class,'create']);
Route::post('/livros', [LivroController::class,'store']);
Para mostrar o formulário html usamos o método create :
public function create(){
return view('livros.create');
}
Formulário html resources/views/livros/create.blade.php com o seguinte conteúdo:
<form method="POST" action="/livros">
@csrf
Título: <input type="text" name="titulo">
Autor: <input type="text" name="autor">
Ano: <input type="text" name="ano">
<button type="submit">Enviar</button>
</form>
Por fim o método store, que salva no banco de dados o cadastro do livro:
public function store(Request $request){
$livro = new Livro;
$livro->titulo = $request->titulo;
$livro->autor = $request->autor;
$livro->ano = $request->ano;
$livro->save();
return redirect('/livros');
}
Já implementamos uma forma de acessar os livros em forma de listagem com o método index, podemos implementar outra forma de acesso individual para cada livro. Rota para acesso ao registro de um livro específico:
Route::get('/livros/{livro}', [LivroController::class,'show']);
Respectivo controller:
public function show(Livro $livro){
return view('livros.show',[
'livro' => $livro
]);
}
Criamos um blade para a rota show resources/views/livros/show.blade.php com o seguinte conteúdo:
Título: {{ $livro->titulo }} <br>
Autor: <i>{{ $livro->autor }}</i> <br>
Ano de publicação: {{ $livro->ano }} <br>
<a href="/livros">Voltar</a>
No index.blade.php podemos criar um link para o show de cada livro:
<a href="/livros/{{ $livro->id}}">{{ $livro->titulo }}</a>
Novamente precisamos de duas rotas para atualizar um registro, uma para exibir o formulário e outra para processar os dados enviados.
Route::get('/livros/{livro}/edit', [LivroController::class,'edit']);
Route::patch('/livros/{livro}', [LivroController::class,'update']);
Implementação no controller:
public function edit(Livro $livro){
return view('livros.edit',[
'livro' => $livro
]);
}
public function update(Request $request, Livro $livro){
$livro->titulo = $request->titulo;
$livro->autor = $request->autor;
$livro->ano = $request->ano;
$livro->save();
return redirect("/livros/{$livro->id}");
}
Criando o blade para edição:
touch resources/views/livros/edit.blade.php
Html para edição no edit.blade.php:
<form method="POST" action="/livros/{{ $livro->id }}">
@csrf
@method('PATCH')
Título: <input type="text" name="titulo" value="{{ $livro->titulo }}">
Autor: <input type="text" name="autor" value="{{ $livro->autor }}">
Ano: <input type="text" name="ano" value="{{ $livro->ano }}">
<button type="submit">Enviar</button>
</form>
Vamos colocar o botão para edição no blade show.blade.php:
<a href="/livros/{{ $livro->id }}/edit">Editar</a> <br>
Rota para delete:
Route::delete('/livros/{livro}', [LivroController::class,'destroy']);
Controller para delete:
public function destroy(Livro $livro)
{
$livro->delete();
return redirect('/livros');
}
Botão html para delete que podemos colocar no blade do show.blade.php:
<form action="/livros/{{ $livro->id }} " method="post">
@csrf
@method('delete')
<button type="submit" onclick="return confirm('Tem certeza?');">Apagar</button>
</form>
Implementação do dusk para testar as operações de CRUD:
php artisan dusk:make LivroCrudTest
Vamos testar sequencialmente as operações:
use App\Models\Livro;
public function test_crud_livros(): void
{
$this->browse(function (Browser $browser) {
// Create
$browser->visit('/livros/create')
->typeSlowly('titulo', '2001: Uma odisséia no espaço')
->typeSlowly('autor', 'Arthur C. Clarke')
->typeSlowly('ano', '1968')
->press('Enviar')
->assertPathIs('/livros')
->assertSee('2001: Uma odisséia no espaço');
// Read
$browser->clickLink('2001: Uma odisséia no espaço')
->assertSee('Arthur C. Clarke')
->assertSee('1968');
// Update
$browser->clickLink('Editar')
->typeSlowly('titulo', '2001: Uma odisséia no espaço - Edição Revisada')
->press('Enviar')
->assertSee('2001: Uma odisséia no espaço - Edição Revisada');
// Delete
$browser->press('Apagar')
->acceptDialog()
->assertPathIs('/livros')
->assertDontSee('2001: Uma odisséia no espaço - Edição Revisada');
});
}
Rodando o teste:
docker exec -it cursolaravel php artisan dusk tests/Browser/LivroCrudTest.php
/frasedodia e o método correspondente que ao ser acessada mostra uma frase aleatória, porém correspondente ao dia da semana. Na próxima reunião, cada membro do grupo (estagiários e funcionários) deve apresentar a implementação na TV.
Instalação do template USP conforme: https://github.com/uspdev/laravel-usp-theme/
Instalação do senhaunica-socialite conforme: https://github.com/uspdev/senhaunica-socialite
Configurações para usar o faker:
APP_URL=http://localhost:8000
SENHAUNICA_KEY=faker
SENHAUNICA_SECRET=faker
SENHAUNICA_CALLBACK_ID=1
SENHAUNICA_ADMINS=111111
SENHAUNICA_DEV="http://auth.local:3141/wsusuario/oauth"
Quando o sistema está produção, você nunca deve alterar uma migration que já foi para o ar, mas sim criar uma migration que altera uma anterior. Por exemplo, se quisermos que adicionar o campo user_id na tabela livros:
php artisan make:migration add_user_id_to_livros_table --table=livros
Nova coluna user_id:
$table->unsignedBigInteger('user_id')->nullable();
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
Aplique a mudança no banco de dados:
php artisan migrate
No controller, é possível capturar o usuário logado assim: auth()->user()->id.
Quando estamos dentro de um método do controller, a forma mais rápida de validação é usando $request->validate, que validará os campos com as condições que passarmos e caso falhe a validação, automaticamente o usuário é retornado para página de origem com todos inputs que foram enviados na requisição, além da mensagens de erro:
$request->validate([
'titulo' => 'required',
'autor' => 'required',
'ano' => 'required|integer',
]);
A função old('titulo') verifica se há input na sessão para o campo titulo, para o create.blade.php:
Título: <input type="text" name="titulo" value="{{old('titulo')}}">
Autor: <input type="text" name="autor" value="{{old('autor')}}">
Ano: <input type="text" name="ano" value="{{old('ano')}}">
E no para o edir.blade.php:
Título: <input type="text" name="titulo" value="{{ old('titulo', $livro->titulo) }}">
Autor: <input type="text" name="autor" value="{{ old('autor', $livro->autor) }}">
Ano: <input type="text" name="ano" value="{{ old('ano', $livro->ano) }}">
A validação, que muitas vezes será idêntica no store e no update, pode ser delegada para um FormRequest. Crie um FormRequest com o artisan:
docker exec -it cursolaravel php artisan make:request LivroRequest
Esse comando gerou o arquivo app/Http/Requests/LivroRequest.php. Como ainda não falamos de permissões, retorne true no método
authorize(). As validações podem ser implementada em rules().
public function rules(){
$rules = [
'titulo' => 'required',
'autor' => 'required',
'ano' => 'required|integer',
];
return $rules;
}
No controler, trocamos as chamadas de Request para LivroRequest:
use App\Http\Requests\LivroRequest;
public function update(LivroRequest $request, Livro $livro){
public function store(LivroRequest $request)
Há situações em que queremos fazer um leve processamento antes de salvar um valor no banco de dados e logo após recuperarmos um valor. Vamos adicionar um campo para preço. Já sabemos como criar uma migration de alteração para alterar a tabela livros:
php artisan make:migration add_preco_column_in_livros --table=livros
E adicionamos na nova coluna:
$table->float('preco')->nullable();
No LivroRequest também deixaremos esse campo como opcional: 'preco' => 'nullable'.
Queremos que o usuário digite, por exemplo, 12,50, mas guardaremos 12.5. Quando quisermos mostrar o valor, vamos fazer a operação inversa. Poderíamos fazer esse tratamento diretamente no controller, mas também podemos usar mutators através no model do livro:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function preco(): Attribute
{
return Attribute::make(
get: fn($value) => number_format($value, 2, ',', ''),
set: fn($value) => str_replace(',','.',$value)
);
}
Revisão do ambiente:
mkdir cursolaravel
cd cursolaravel
docker run --rm -it \
-v $(pwd):/app \
-u $(id -u):$(id -g) \
composer:latest \
composer create-project laravel/laravel .
curl -L https://fflch.github.io/assets/laravel/Dockerfile -o Dockerfile
curl -L https://fflch.github.io/assets/laravel/docker-compose.yml -o docker-compose.yml
docker compose up --build
Arquivos prontos para subir o ambiente com o conteúdo visto até então, ou sejá, CRUD do model Livros e testes no Dusk.
mkdir -p app/Http/Requests
mkdir -p app/Console/Commands
mkdir -p resources/views/livros
mkdir -p tests/Browser
# .env
curl -L https://fflch.github.io/assets/laravel/crud_livros/env -o .env
# Migration
curl -L https://fflch.github.io/assets/laravel/crud_livros/2026_07_30_131403_create_livros_table.php \
-o database/migrations/2026_07_30_131403_create_livros_table.php
# Models
curl -L https://fflch.github.io/assets/laravel/crud_livros/Livro.php \
-o app/Models/Livro.php
curl -L https://fflch.github.io/assets/laravel/crud_livros/User.php \
-o app/Models/User.php
# Controller
curl -L https://fflch.github.io/assets/laravel/crud_livros/LivroController.php \
-o app/Http/Controllers/LivroController.php
curl -L https://fflch.github.io/assets/laravel/crud_livros/IndexController.php \
-o app/Http/Controllers/IndexController.php
# Form Request
curl -L https://fflch.github.io/assets/laravel/crud_livros/LivroRequest.php \
-o app/Http/Requests/LivroRequest.php
# Artisan Command
curl -L https://fflch.github.io/assets/laravel/crud_livros/ImportaLivros.php \
-o app/Console/Commands/ImportaLivros.php
# Views
curl -L https://fflch.github.io/assets/laravel/crud_livros/home.blade.php \
-o resources/views/home.blade.php
curl -L https://fflch.github.io/assets/laravel/crud_livros/index.blade.php \
-o resources/views/livros/index.blade.php
curl -L https://fflch.github.io/assets/laravel/crud_livros/create.blade.php \
-o resources/views/livros/create.blade.php
curl -L https://fflch.github.io/assets/laravel/crud_livros/edit.blade.php \
-o resources/views/livros/edit.blade.php
curl -L https://fflch.github.io/assets/laravel/crud_livros/show.blade.php \
-o resources/views/livros/show.blade.php
# Rotas
curl -L https://fflch.github.io/assets/laravel/crud_livros/web.php \
-o routes/web.php
# Teste
curl -L https://fflch.github.io/assets/laravel/crud_livros/LivroCrudTest.php \
-o tests/Browser/LivroCrudTest.php
# Senha Única
docker exec -it cursolaravel composer require uspdev/senhaunica-socialite
docker exec -it cursolaravel php artisan vendor:publish --provider="Uspdev\SenhaunicaSocialite\SenhaunicaServiceProvider" --tag="migrations"
docker exec -it cursolaravel php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
docker exec -it cursolaravel php artisan migrate
# dusk
docker exec -it cursolaravel composer require --dev laravel/dusk
docker exec -it cursolaravel php artisan dusk:install
docker exec -it cursolaravel php artisan dusk:chrome-driver
# theme
docker exec -it cursolaravel composer require uspdev/laravel-usp-theme
docker exec -it cursolaravel php artisan vendor:publish --provider="Uspdev\UspTheme\ServiceProvider" --tag=config
docker exec -it cursolaravel php artisan vendor:publish --provider="Uspdev\UspTheme\ServiceProvider" --tag=assets --force
# outras libs
docker exec -it cursolaravel composer require league/csv
Acessar o laravel criado: http://127.0.0.1:8000/
docker exec -it cursolaravel php artisan dusk tests/Browser/LivroCrudTest.php
Acessar http://localhost:7900/ com senha secret e assistir o teste rodando.
Importando csv com os livros:
docker exec -it cursolaravel php artisan app:importa-livros
Na tabela livros já inserimos o campo user_id apontando para tabela users. Vamos agora criar as relações (one-to-many) no laravel:
Model Livro:
public function user()
{
return $this->belongsTo(User::class);
}
Nos fornece o poder de acessar o objeto usuário a partir do livro, exemplo no show.blade.php
<p>
Livro cadastrado em {{ $livro->created_at->format('d/m/Y H:i') }} <br>
Última atualização em {{ $livro->updated_at->format('d/m/Y H:i') }} por <b>{{ $livro->user?->name }}</b>
</p>
Model User:
public function livros()
{
return $this->hasMany(Livro::class);
}
Nos fornece o poder de acessar todos os objetos de livros a partir de um usuário, exemplo no show.blade.php:
<div>
@if($livro->user)
Outros livros cadastrados ou editados por {{ $livro->user?->name }}:
<ul>
@foreach($livro->user->livros as $outro_livro)
<li>{{ $outro_livro->titulo }}</li>
@endforeach
</ul>
@endif
</div>
1 - Imagem do docker
Para construção da imagem baseada na tag (versão) criar o arquivo .github/workflows/docker.yml:
Arquivo modelo docker.yml
2 - testes no dusk
Para rodar os testes no dusk baseado na configuração do docker-compose.yml, criar o arquivo .github/workflows/dusk.yml:
Arquivo modelo dusk.yml
É necessário alterar a variável SERVICE_NAME no dusk.yml colocando o nome do sistema.