Creating a CLI Application With Laravel and Docker

Last updated on by

Creating a CLI Application With Laravel and Docker image

Laravel provides a robust CLI framework built on top of the popular Symfony Console component, which brings the best features of Laravel to the command line. While Laravel is traditionally used to create web applications, some applications need robust CLI commands that you can run via Docker in production environments.

If you are building a CLI-only project, you could also consider using the community project Laravel Zero. Everything we discuss in this article will work with Laravel or Laravel Zero (with a few tweaks to the Docker image).

Setting up the Project

We will build a small stock checker CLI (using the Polygon.io API) that you can run via Docker, which provides some subcommands to do things like check stocks. We will build a stock:check command that will look up stocks for a given date using the stock's symbol:

php artisan stock:check AAPL

At the time of writing, Polygon.io provides a free basic API plan that allows 5 API calls per minute and goes back two years ago. If you want to follow along, you'll need to have an API key. Using an API key will also let us illustrate how to configure secrets to use with our Docker image.

The first thing we'll do is create the Laravel project. If you are following along, you will need to Install PHP and the Laravel installer:

laravel new stock-checker --git --no-interaction

We don't need any starter kits since our application is just a CLI, so we use the --no-interaction flag to just accept all the defaults. If you can run php artisan inspire after creating the stock-checker project, you're ready to get started:

php artisan inspire
 
“ I begin to speak only when I am certain what I will say is not better left unsaid. ”
— Cato the Younger

Lastly, we need to create a few files to work with Docker during development, though consumers don't necessarily need anything other than a container runtime to use the application:

mkdir build/
touch \
.dockerignore \
compose.yaml \
build/Dockerfile

We created the Dockerfile file in the build folder. I prefer to store Docker configuration files in a subdirectory to neatly organize things like INI configuration files and any Docker-related project files.

We have everything needed to get started. In the next section, we'll scaffold a command and set up the application to run with Docker.

Creating the CLI Command

Our application will start with one check command to look up US stock details for a given stock symbol. We won't focus on the contents of this file, but if you want to follow along, create a command file with Artisan:

php artisan make:command CheckStockCommand

Add the following code to the newly crated CheckStockCommand.php file found in the app/Console/Commands folder:

<?php
 
namespace App\Console\Commands;
 
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Console\Command;
 
class CheckStockCommand extends Command
{
protected $signature = 'stock:check {symbol} {--d|date= : The date to check the stock price for}';
 
protected $description = 'Check stock price for a given symbol.';
 
public function handle()
{
$symbol = Str::upper($this->argument('symbol'));
 
// Get the most recent trading weekday.
$date = now()->previousWeekday();
 
if ($dateOption = $this->option('date')) {
$date = Carbon::parse($dateOption);
if ($date->isToday() || $date->isFuture()) {
$this->error('Date must be in the past.');
return;
}
}
 
if ($date->lt(now()->subYear())) {
$this->error('Date must be within the last year.');
return;
}
 
// Find the Ticker Details
$ticker = $this->getClient()
->withUrlParameters(['symbol' => $symbol])
->withQueryParameters(['date' => $date->toDateString()])
->throw()
->get("https://api.polygon.io/v3/reference/tickers/{symbol}")
->json('results');
 
$openClose = $this->getClient()
->withUrlParameters([
'symbol' => $symbol,
'date' => $date->toDateString()
])
->get("https://api.polygon.io/v1/open-close/{symbol}/{date}?adjusted=true");
 
if ($openClose->failed()) {
$this->error("Could not retrieve stock data.\nStatus: " . $openClose->json('status') . "\nMessage: " . $openClose->json('message') . "\n");
return;
}
 
 
$this->info("Stock: {$ticker['name']} ({$ticker['ticker']})");
$this->info("Date: {$date->toDateString()}");
$this->info("Currency: {$ticker['currency_name']}");
$this->table(['Open', 'Close', 'High', 'Low'], [
[
number_format($openClose['open'], 2),
number_format($openClose['close'], 2),
number_format($openClose['high'], 2),
number_format($openClose['low'], 2),
],
]);
}
 
protected function getClient(): PendingRequest
{
return Http::withToken(config('services.polygon.api_key'));
}
}

The console command looks up a stock symbol for a past date within the last year and returns basic information about the stock for that given date. For this command to work, we need to define a service configuration and configure a valid key. Add the following configuration to the config/services.php file that the command will use to configure the API key:

// config/services.php
return [
// ...
'polygon' => [
'api_key' => env('POLYGON_API_KEY'),
],
];

Make sure to add the POLYGON_API_KEY to your .env file, and add the env variable to .env.example as a blank value:

# .env
POLYGON_API_KEY="<your_secret_key>"
 
# .env.example
POLYGON_API_KEY=

If you run the command locally, you'll get something like this if you enter a valid stock symbol:

php artisan stock:check AAPL
Stock: Apple Inc. (AAPL)
Date: 2024-10-25
Currency: usd
+--------+--------+--------+--------+
| Open | Close | High | Low |
+--------+--------+--------+--------+
| 229.74 | 231.41 | 233.22 | 229.57 |
+--------+--------+--------+--------+

We've verified that the command works, and now it's time to see how we can create a Docker image to house our CLI. Docker allows anyone to use our CLI without any of the complicated knowledge about setting up a runtime for it.

The last thing we'll do before creating the Docker image is add the .env file to the .dockerignore file we created during setup. Add the following line to the .dockerignore file so that we don't copy sensitive secrets we have locally during a build:

.env

Creating a Docker Image for a CLI

We are ready to configure the CLI to work with Docker. There are a few typical use cases for a CLI-based Docker image:

  1. Running a CLI with Docker during development
  2. Running a CLI as part of a deployment in containers
  3. Distributing a CLI for end-users

All of the above use cases apply to how we configure the Dockerfile, and this article will demonstrate a few ways you can structure the image for a CLI. We will consider running our CLI as one single command or providing a way for users to run multiple subcommands.

Our Dockerfile is based on the official PHP CLI image, and will use the ENTRYPOINT instruction to make the stock:check command the single command that the image can run. Without overriding the entrypoint, all commands issued to our image will run in the context of the stock:check command:

FROM php:8.3-cli-alpine
 
RUN docker-php-ext-install pcntl
 
COPY . /srv/app
WORKDIR /srv/app
ENTRYPOINT ["php", "/srv/app/artisan", "stock:check"]
CMD ["--help"]

Note: installing the pcntl extension in Docker CLI projects allows graceful shutdown of Docker containers using process signals. We don't demonstrate that in this command, but if you have a long-running daemon, you will need to handle graceful shutdowns in any server environment, including containers.

The ENTRYPOINT instruction specifies the command executed when the container is started. What's neat about it in the context of our CLI is that all commands we pass to the container when we run it are appended to the entrypoint. Let me try to illustrate:

# ENTRYPOINT # Default CMD
php artisan stock:check --help
 
# Above is equivalent to running this:
docker run --rm stock-checker
 
# ENTRYPOINT # Override CMD
php artisan stock:check AAPL
 
# Above is equivalent to running this:
docker run --rm stock-checker AAPL

We are demonstrating that the ENTRYPOINT allows running one command, but if you make this small tweak, you can run any of the commands available in the Artisan console:

FROM php:8.3-cli-alpine
 
RUN docker-php-ext-install pcntl
 
COPY . /srv/app
WORKDIR /srv/app
ENTRYPOINT ["php", "/srv/app/artisan"]
CMD ["list"]

Now the entry point is artisan, giving us the ability to run any command within the Artisan console. If you are distributing a CLI to other users, you don't want to expose commands other than the ones you've defined, but as it stands now, they can run all commands available to your application.

Running the php artisan list command will now be the default when running the Docker image without any command arguments. We can easily run our stock checker command and other commands we make for our CLI as follows:

docker build -t stock-checker -f build/Dockerfile .
 
# Run the stock checker
export POLYGON_API_KEY="<your_key>"
docker run --rm --env POLYGON_API_KEY stock-checker stock:check AAPL

The stock:check and AAPL parts are make up the CMD now. Before, the stock:check was the only command the CLI could run without overriding the entrypoint (which you can do via the --entrypoint= flag with docker run.

Notice that our CLI requires an environment variable to configure credentials. Using the --env flag we can pass in a local ENV variable that we exported. Depending on your application needs, you could provide a configuration file that the CLI reads from a secret volume mount. For the convenience of this article, we use Docker's built-in ENV capabilities to run the CLI.

Running Laravel Web Apps as a CLI

If you need to run a CLI command in your Laravel app via Docker, you'll typically have a php-fpm Docker image containing your web app/API. Instead of building a separate CLI image, you can tweak the entrypoint and command to run as an Artisan console instead of a web app. Everything will look similar to what we have, and the benefit is that you can reuse your web app image to also run the CLI:

docker run --rm my-app --entrypoint=/srv/app/artisan stock:check AAPL

Learn More

That's the basics of running a Laravel CLI with Docker using the ENTRYPOINT to define the base command that our Docker image will use to run commands. You can learn more about entrypoint and cmd in the official Dockerfile reference.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Laravel Idea for PhpStorm logo

Laravel Idea for PhpStorm

Ultimate PhpStorm plugin for Laravel developers, delivering lightning-fast code completion, intelligent navigation, and powerful generation tools to supercharge productivity.

Laravel Idea for PhpStorm
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
JetShip - Laravel Starter Kit logo

JetShip - Laravel Starter Kit

A Laravel SaaS Boilerplate and a starter kit built on the TALL stack. It includes authentication, payments, admin panels, and more. Launch scalable apps fast with clean code, seamless deployment, and custom branding.

JetShip - Laravel Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Streamlining Route Parameters in Laravel Using URL Defaults image

Streamlining Route Parameters in Laravel Using URL Defaults

Read article
Flexible Docker Images with PHP INI Environment Variables image

Flexible Docker Images with PHP INI Environment Variables

Read article
Dynamic Form Validation in Laravel with prohibited_if image

Dynamic Form Validation in Laravel with prohibited_if

Read article
Add Approvals to Your Laravel Application image

Add Approvals to Your Laravel Application

Read article
Enhancing Data Processing with Laravel's transform() Method image

Enhancing Data Processing with Laravel's transform() Method

Read article
Get Xdebug Working With Docker and PHP 8.4 in One Minute image

Get Xdebug Working With Docker and PHP 8.4 in One Minute

Read article