Laravel Temporary URLs & MinIO

I really like Laravel as framework, because it simplifies a lot of stuff – instead of focusing on every detail, we can just use a lot of tools and focus more or business logic. Of course, sometimes they are a bit limited, and we need to extend them or even replace, but overall, it is a very solid solution with amazing community support. The problem starts if something is not well documented or even documented in wrong way and in effect, we have some hidden functionalities. Last time I found something like that related to filesystems and MinIO driver, finally decided to write separate blog post about that, because it may help many people.

Problem Description

Ok, so what is the problem? According to the official Laravel documentation, using temporary URLs with MinIO driver is not possible. It means pretty big drawback, because in most of cases we will not want to use real storage solutions for local or testing environment and in effect, we can completely block this feature for some specific envs:

Generating temporary storage URLs via the temporaryUrl method is not supported when using MinIO.

Official Laravel 10 Documentation

Why Does MinIO Not Support TemporaryUrl?

Everything depends on driver responsible for supporting MinIO storage, i.e. the same as for Amazon S3. If you will check vendor files, you will realize it always requires endpoint in client config. This endpoint is used by API to call MinIO/S3 to save or get files. The same endpoint is also used to generate temporaryUrls. Such URLs are signed and any change will cause they will stop work. So, if we will try to modify subdomain in signed URL, it will not work anymore. It makes a lot of sense from security perspective.

But how does it affect our situation? Let’s imagine two possible options. First is just remote environment with even totally separate MinIO instance somewhere and working with its own domain or subdomain. In this scenario we need to communicate API with this service and one endpoint in config will work properly without any issues. It is just required because it is separate service outside our API system. Second is local, let’s say we use docker, or even remote solution, but similar, maybe cloud, maybe internal network or Kubernetes. In such example our MinIO server will probably have some internal name like minio-server or minio-service.

We need to expose main API port (default 9000) to allow communication, without that it will not be possible to even access generated temporaryUrls and it is fine. Let’s go down into communication. We can use minio-service as endpoint for API config and of course it will work without issues – client will send files to API, it will connect to MinIO and store file. Perfect! But what about temporaryUrls? The same, API will connect to MinIO, will retrieve response and will send it back to client. Client will use received URL and… Houston, we have a problem! URL includes internal name of minio-service (example: https://minio-service/{TEMPORARY_URL}) so our browser cannot access and will trigger error. Ok, next question is: how we can solve this issue?

Solution A: Exposing MinIO

First option is to create subdomain for MinIO. It may be a bit more complicated on local, but on remote solutions this is required anyway if we want to use temporaryUrls – clients need to have access to this service. After we can use FQDN as endpoint, so instead of minio-server, we will use for example https://minio.mydomain:9000. After that change everything will work correctly so we can mark this as done and go for holidays. Yep? Not exactly. Such approach has one problem: as I described above, endpoint is used for the whole communication, so please analyze file upload: client uploads file to our API. Then API will use this endpoint so will send file to MinIO… But not by using internal network, it will use internet so file will go outside our network and then go back to MinIO. It is far from perfect because causes unnecessary latency and additional networking costs.

Solution B: Separate Config

Second – and right now the best option I found, is to use separate MinIO endpoint for uploading/receiving files and another one just to generate temporaryUrls. The simplest method is to create additional, almost identical config with just changed endpoint. You can add additional options for client to disable SSL verification because it is not required to use it in internal network:

// config/filesystems.php
'minio' => [
    'endpoint' => env('MINIO_ENDPOINT', 'http://minio-service:9000'),
    'options' => [
        'verify' => env('MINIO_SSL_VERIFY', false),
    ],
    'http' => [
       'verify' => env('MINIO_SSL_VERIFY', false),
    ],
],

'minio-temporaryurls' => [
    'endpoint' => env('MINIO_ENDPOINT_TEMPORARYURLS', 'https://minio.myservice.com'),
],

// REST OF VALUES SHOULD BE THE SAME
// First config, standard usage
Storage::disk('minio')->put('myfile', $content);
$content = Storage::disk('minio')->get('myfile');

// Second config just for temporaryUrls
$url = Storage::disk('minio-temporaryurls')->temporaryUrl(
    'myfile', now()->addMinutes(5)
);

Such configuration means for all real big tasks we will use internal network, only for temporaryUrls API will access MinIO using global network: but it can be also handled properly inside our system, depends on configuration. Anyway, temporaryUrls will work properly – so documentation is not 100% valid, it is possible to use them with MinIO driver, but it requires some additional work.

Of course, there are better methods i.e. more clean code – you can extend S3 driver or hide the whole storage behind custom internal facade. This post is not about them, and I will stop there, you need to decide and use the best solution for your needs.