Background

We store our image resources and other static assets using S3. To reduce code complexity, we’ve been using the same S3 environment for development and testing stages, and everything had been running smoothly. However, suddenly one day, uploading files to S3 from our test server in China started timing out, and this problem couldn’t be resolved.

Initially, we considered uploading files to the Storage directory within the project during testing, but this made configuration and URL generation very complicated and wasn’t conducive to DevOps.

In our projects, we extensively use custom configurations or ENV variables to concatenate static resource URLs, resulting in poor code maintainability. Additionally, to ensure consistent access, we would need to set up a separate web service for uploaded files.

That’s when I remembered an open-source project I had seen on GitHub called MinIO, which is today’s main topic.

Introduction to MinIO

MinIO is an S3-like storage service developed in Go. Why use it to replace S3? Because it’s compatible with S3’s API. This reduces complexity when integrating it into projects, especially when generating resource URLs.

Deploying MinIO

I’ll use Docker Compose for deployment. Here’s the configuration file:

services:
  minio:
  image: minio/minio:latest
  restart: always
  hostname: minio
  container_name: minio
  ports:
    - 9000:9000
    - 9001:9001
  volumes:
    - /usr/www/data/minio:/data
  environment:
    - MINIO_DOMAIN=example.dev # Enable DNS Style Bucket mode
    - MINIO_ROOT_USER=${MINIO_ROOT_USER}
    - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
  command: server /data --console-address ":9001"

Setting up Nginx proxy:

server {
    listen 80;
    listen [::]:80;
    server_name minio.example.dev bucket.example.dev minio-console.example.dev;

    location / {
        return 301 https://$host$request_uri;
    }
}

# Domain used for file uploads
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name minio.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    # gzip
    gzip                    on;
    gzip_vary               on;
    gzip_proxied            any;
    gzip_comp_level         6;
    gzip_types              text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

    location / {
        proxy_pass http://minio:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Domain for MinIO Bucket, so the bucket name doesn't need to be included in the URL when accessing uploaded files
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name bucket.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    # gzip
    gzip                    on;
    gzip_vary               on;
    gzip_proxied            any;
    gzip_comp_level         6;
    gzip_types              text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

    location / {
        # When DNS Style Bucket is not enabled, you need to use the Bucket created in MinIO as the proxy Endpoint
        proxy_pass http://minio:9000/bucket/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Proxy configuration for MinIO admin console
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name minio-console.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    location / {
        proxy_pass http://minio:9001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /ws {
        proxy_pass http://minio:9001;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        chunked_transfer_encoding off;
    }
}

MinIO also supports DNS Style Bucket, but it’s not enabled by default. To enable it, use the environment variable MINIO_DOMAIN=domain.com to set the FQDN for MinIO. For example, if your bucket name is assets, the access method would be assets.domain.com. After enabling DNS Style Bucket mode, you no longer need to use Nginx to map subdomains to paths. Note that this method requires resolving both TLD and *.TLD to the MinIO proxy server.

For example, using wildcard domains in Nginx as bucket access domains:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name *.example.dev;

    # SSL
    ssl_certificate certs/fullchain.cer;
    ssl_certificate_key certs/example.dev.key;

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_buffering off;
    client_max_body_size 0;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    ssl_trusted_certificate /etc/nginx/certs/ca-bundle.trust.crt;

    include components/security.conf;

    # gzip
    gzip                    on;
    gzip_vary               on;
    gzip_proxied            any;
    gzip_comp_level         6;
    gzip_types              text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

    location / {
        # When DNS Style Bucket is enabled, you don't need to use the Bucket created in MinIO as the URL PATH.
        proxy_pass http://minio:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

If you’re using Traefik, you can refer to the following configuration:

services:
  minio:
    image: minio/minio:latest
    labels:
      - traefik.enable=true

      - traefik.http.routers.minio.tls=true
      - traefik.http.routers.minio.tls.certresolver=step-ca
      - traefik.http.routers.minio.rule=Host(`example.dev`)
      - traefik.http.routers.minio.service=minio
      - traefik.http.routers.minio.entrypoints=http,https
      - traefik.http.services.minio.loadbalancer.server.port=9000

      - traefik.http.routers.minio-console.tls=true
      - traefik.http.routers.minio-console.tls.certresolver=step-ca
      - traefik.http.routers.minio-console.rule=Host(`minio-console.example.dev`) || PathPrefix(`/ws`)
      - traefik.http.routers.minio-console.service=minio-console
      - traefik.http.routers.minio-console.entrypoints=http,https
      - traefik.http.services.minio-console.loadbalancer.server.port=9001

      - traefik.http.routers.bucket.tls=true
      - traefik.http.routers.bucket.tls.certresolver=step-ca
      - traefik.http.routers.bucket.tls.domains[0].main=example.dev
      - traefik.http.routers.bucket.tls.domains[0].sans=*.example.dev
      - traefik.http.routers.bucket.rule=HostRegexp(`^.+.example.dev$`)
      - traefik.http.routers.bucket.service=bucket
      - traefik.http.routers.bucket.priority=10
      - traefik.http.routers.bucket.entrypoints=http,https
      - traefik.http.services.bucket.loadbalancer.server.port=9000
    restart: no
    hostname: minio
    container_name: minio
    networks:
      - traefik
    volumes:
      - minio-data:/data
    environment:
      TZ: Asia/Shanghai
      MINIO_DOMAIN: minio.svc.dev
      MINIO_ROOT_USER: developer
      MINIO_ROOT_PASSWORD: Developer@1994
    command: server /data --console-address ":9001"

Important Notes

  1. Make sure to add the relevant proxy_set_header configurations, otherwise MinIO cannot be accessed normally;
  2. Assign a separate domain for the Bucket to perfectly simulate S3 access URLs;

Integrating MinIO in Laravel

Installing Dependencies

composer require league/flysystem-aws-s3-v3:~1.0

Modifying Configuration File

Modify the configuration in config/filesystems.php as follows:

's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'ap-northeast-1'),
    'bucket' => env('AWS_BUCKET', 'neox'),
    'url' => env('AWS_URL'),
    'endpoint' => env('AWS_ENDPOINT'),
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false)
]

Modifying Environment Configuration

Modify the configuration in the .env file as follows:

FILESYSTEM_DRIVER=s3
AWS_BUCKET=static # Your bucket name
AWS_URL=https://bucket.example.dev # Prefix used for generating URLs
AWS_ENDPOINT=https://minio.example.dev # URL used for uploading files
AWS_DEFAULT_REGION=ch-shanghai # Region configured in MinIO backend
AWS_ACCESS_KEY_ID=MINIO_ACCESS_KEY_ID # User Access Key ID created in MinIO backend
AWS_SECRET_ACCESS_KEY=MINIO_ACCESS_KEY_SECRET # User Access Key SECRET created in MinIO backend
AWS_USE_PATH_STYLE_ENDPOINT=true # Must be true for perfect S3 compatibility

Uploading Files

$uri = Storage::put($path, $request->file('file'), ['visibility' => 'public']);
if ($uri) {
    return response()->json([
        'uri' => $uri,
        'url' => Storage::url($uri),
        'filename' => Str::afterLast($uri, '/')
    ]);
}

Response example:

{
    "uri": "trend/reports/8q472L1asBz06mM7VK7i4gd1Kyen4eWRaAcxlmX5.jpg",
    "url": "https://bucket.example.dev/trend/reports/8q472L1asBz06mM7VK7i4gd1Kyen4eWRaAcxlmX5.jpg",
    "filename": "8q472L1asBz06mM7VK7i4gd1Kyen4eWRaAcxlmX5.jpg"
}

At this point, we’ve completed the integration and use of MinIO. In the production environment, we just need to modify the .env configuration items to match the production environment settings.

Coming Soon

Since our test server data is cloned from the production environment, many image resources are stored in S3. So how do we synchronize files from S3 to the MinIO service on the test server?

In the future, I’ll share my experience using MinIO CLI for synchronization, as well as using events and message queues for resource synchronization.

I hope this is helpful, Happy hacking…