Saltar al contenido
infraestructura / · 4 min

Montar tu propio S3 con MinIO detras de nginx, y conectarlo a Laravel

Un contenedor, un subdominio con TLS y Laravel guardando archivos como si fuera Amazon. Incluye las cuatro lineas de nginx sin las que las subidas se cortan en 1 MB.

Administrador · Desarrollador Laravel
Montar tu propio S3 con MinIO detras de nginx, y conectarlo a Laravel

S3 sin AWS: un contenedor de MinIO, un subdominio con TLS y Laravel guardando archivos ahí como si fuera Amazon. Al final tendrás una API S3 pública y una consola de administración que no lo es.

Doy por hecho que ya tienes Docker, nginx de reverse proxy y certbot funcionando en un VPS.

1. El registro DNS, primero

Crea un registro A para s3.tudominio.com apuntando a tu servidor. Hazlo antes que nada: certbot va a necesitar resolverlo y la propagación tarda.

2. El stack

# ~/apps/minio/docker-compose.yml
services:
  minio:
    image: minio/minio:RELEASE.2025-04-22T22-12-26Z
    container_name: minio
    restart: unless-stopped
    command: server /data --console-address ":9001"
    env_file: .env
    ports:
      - "127.0.0.1:9000:9000"   # API S3
      - "127.0.0.1:9001:9001"   # consola
    volumes:
      - minio_data:/data

volumes:
  minio_data:

Tres decisiones que conviene entender:

La versión está fija y es vieja a propósito. Desde mediados de 2025 la consola de la edición comunitaria quedó reducida a login e información: ya no permite crear buckets ni llaves. Esa release es la última con la interfaz completa. Como no recibe parches, la consola no se publica a internet — verás en el paso 5 que solo se llega por túnel.

Los dos puertos van a loopback. Sin el prefijo 127.0.0.1, Docker escribe reglas de iptables que se saltan tu firewall y publicas MinIO al mundo sin enterarte.

Volumen con nombre, no bind mount. MinIO es quisquilloso con los permisos del directorio de datos; un volumen se los administra él.

El .env, con permisos cerrados:

cd ~/apps/minio
printf 'MINIO_ROOT_USER=admin\nMINIO_ROOT_PASSWORD=%s\n' "$(openssl rand -hex 20)" > .env
chmod 600 .env
docker compose up -d

3. nginx: el detalle que rompe las subidas

# /etc/nginx/conf.d/s3.conf
server {
    server_name s3.tudominio.com;

    location / {
        proxy_pass http://127.0.0.1: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;

        # Sin estas cuatro, las subidas se cortan en 1 MB con un 413.
        client_max_body_size       0;
        proxy_request_buffering    off;
        proxy_buffering            off;
        chunked_transfer_encoding  off;
    }
}

Las cuatro líneas del final no son opcionales. Por omisión nginx acumula el cuerpo entero de la petición en disco antes de pasarlo, lo que con archivos grandes es lento y con el límite por defecto directamente falla. Apagando el buffering, nginx transmite mientras recibe.

sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d s3.tudominio.com

4. El bucket y sus llaves

MinIO trae mc, su cliente, dentro del contenedor:

docker exec -it minio mc alias set local http://127.0.0.1:9000 admin 'TU_PASSWORD'
docker exec -it minio mc mb local/archivos

No uses las credenciales root en la aplicación. Crea una llave de servicio con acceso solo a ese bucket; si se filtra, se revoca sin tocar nada más.

5. La consola, por túnel

Desde tu máquina, en una ventana que dejas abierta:

ssh -L 9001:127.0.0.1:9001 usuario@tu-servidor

Y en el navegador: http://127.0.0.1:9001.

Escribe 127.0.0.1, no localhost. Chrome y Edge en Windows resuelven localhost a ::1 (IPv6) y el túnel de ssh escucha en IPv4: la página no carga y parece que el túnel falló. Es el error que más tiempo me ha hecho perder de toda esta guía.

6. Laravel

composer require league/flysystem-aws-s3-v3
AWS_ACCESS_KEY_ID=la-llave-de-servicio
AWS_SECRET_ACCESS_KEY=el-secreto
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=archivos
AWS_ENDPOINT=https://s3.tudominio.com
AWS_USE_PATH_STYLE_ENDPOINT=true

AWS_USE_PATH_STYLE_ENDPOINT=true es obligatorio. AWS direcciona como bucket.dominio.com; MinIO como dominio.com/bucket. Sin la bandera, el SDK construye URLs de un subdominio que no existe y todo falla con errores de DNS que no parecen tener nada que ver.

La región da igual, pero tiene que estar: el SDK la exige para firmar.

Storage::disk('s3')->put('facturas/2026/f-001.pdf', $contenido);

Cuidado con FILESYSTEM_DISK

Es tentador poner FILESYSTEM_DISK=s3 y olvidarse. Eso cambia el disco por omisión de toda la aplicación, y los archivos que ya estaban en storage/app no se migran solos: quedan invisibles. En una aplicación que ya tiene datos, mejor pedir el disco explícitamente donde lo quieras.

Prueba de extremo a extremo

Esta sola valida dominio, certificado, nginx y llaves de un golpe:

docker exec -it minio mc alias set publico https://s3.tudominio.com 'LLAVE' 'SECRETO'
docker exec -it minio mc cp /etc/hostname publico/archivos/prueba.txt
docker exec -it minio mc ls publico/archivos

Si eso responde, ya tienes tu S3.

#laravel #arquitectura #despliegue
Comentarios 0

Nadie ha comentado todavía. Estrena la sección.

Deja tu comentario
Tu correo no se publica. Reviso los comentarios antes de publicarlos.

Seguir leyendo