# Pacote para revisão — Laravel + Docker (infra + app)

Copie este arquivo inteiro para outra IA revisar.

---

## Visão geral da estrutura

```text
raiz-do-repo/
├── .dockerignore          # contexto de build = raiz
├── .gitignore
├── app/                   # Laravel (composer create-project)
│   ├── .env               # não versionado; Compose lê mysql/redis/app
│   └── .gitignore
└── infra/
    ├── docker-compose.yml, docker-compose.dev.yml
    ├── Dockerfile, Dockerfile.dev, .env.example
    ├── docker/php/*.ini, nginx/*.conf, INSTALACAO.md
```

**Convenções:** `docker compose` a partir de `infra/`. Build `context: ..` (raiz). Volumes: `../app` → `/var/www/html` no container.

---

## Arquivo: `.gitignore`

```
/app/.env
/app/.env.*
!/app/.env.example
.DS_Store
Thumbs.db
```

## Arquivo: `.dockerignore`

```
.git
**/.git
app/node_modules
app/vendor
app/storage/logs
app/storage/framework/cache
app/storage/framework/sessions
app/storage/framework/views
app/bootstrap/cache
*.log
.DS_Store
Thumbs.db
```

## Arquivo: `app/.gitignore`

```
.env
.env.backup
/vendor/
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
```

## Arquivo: `app/.env`

```dotenv
# App
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8080

# Banco de dados
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel
DB_PASSWORD=laravel_password
MYSQL_ROOT_PASSWORD=root_password
MYSQL_DATABASE=laravel_db
MYSQL_USER=laravel
MYSQL_PASSWORD=laravel_password

# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis_password

# Cache / Queue / Session
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis```

## Arquivo: `infra/docker-compose.yml`

```yaml
version: '3.8'

services:
  app:
    build:
      context: ..
      dockerfile: infra/Dockerfile
    container_name: laravel_app
    restart: unless-stopped
    working_dir: /var/www/html
    networks:
      - laravel_network
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    env_file:
      - ../app/.env
    mem_limit: 512m
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"
    healthcheck:
      test: ["CMD-SHELL", "php artisan about > /dev/null 2>&1"]
      interval: 10s
      timeout: 3s
      retries: 3

  queue:
    build:
      context: ..
      dockerfile: infra/Dockerfile
    container_name: laravel_queue
    restart: unless-stopped
    working_dir: /var/www/html
    command: php artisan queue:work --tries=3 --backoff=5 --timeout=90 --sleep=3 --max-time=3600 --verbose
    env_file:
      - ../app/.env
    networks:
      - laravel_network
    depends_on:
      redis:
        condition: service_healthy
    mem_limit: 512m
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"

  scheduler:
    build:
      context: ..
      dockerfile: infra/Dockerfile
    container_name: laravel_scheduler
    restart: unless-stopped
    working_dir: /var/www/html
    command: php artisan schedule:work
    env_file:
      - ../app/.env
    networks:
      - laravel_network
    depends_on:
      app:
        condition: service_started
    mem_limit: 128m
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"

  nginx:
    image: nginx:1.27-alpine
    container_name: laravel_nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ../app:/var/www/html
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    networks:
      - laravel_network
    depends_on:
      app:
        condition: service_healthy
    mem_limit: 128m
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"

  mysql:
    image: mysql:8.0
    container_name: laravel_mysql
    restart: unless-stopped
    command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    env_file:
      - ../app/.env
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - laravel_network
    mem_limit: 512m
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:7-alpine
    container_name: laravel_redis
    restart: unless-stopped
    env_file:
      - ../app/.env
    command: >
      sh -c "exec redis-server --requirepass \"$$REDIS_PASSWORD\" --appendonly yes"
    networks:
      - laravel_network
    volumes:
      - redis_data:/data
    mem_limit: 128m
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"
    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 3

networks:
  laravel_network:
    driver: bridge

volumes:
  mysql_data:
  redis_data:
```

## Arquivo: `infra/docker-compose.dev.yml`

```yaml
version: '3.8'

services:
  app:
    build:
      context: ..
      dockerfile: infra/Dockerfile.dev
    container_name: laravel_app_dev
    volumes:
      - ../app:/var/www/html
    env_file:
      - ../app/.env
    environment:
      - APP_ENV=local
      - APP_DEBUG=true
      - XDEBUG_MODE=debug
    extra_hosts:
      - "host.docker.internal:host-gateway"

  nginx:
    container_name: laravel_nginx_dev
    ports:
      - "8080:80"
    volumes:
      - ../app:/var/www/html
      - ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro

  mysql:
    container_name: laravel_mysql_dev
    ports:
      - "127.0.0.1:3307:3306"
    env_file:
      - ../app/.env

  redis:
    container_name: laravel_redis_dev
    ports:
      - "127.0.0.1:6380:6379"
    env_file:
      - ../app/.env
```

## Arquivo: `infra/Dockerfile`

```dockerfile
# ================================
# Stage 1 — builder
# ================================
FROM php:8.3-fpm AS builder

WORKDIR /var/www/html

RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip \
    && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN pecl install redis && docker-php-ext-enable redis
RUN docker-php-ext-install opcache

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

COPY app/composer.json app/composer.lock ./
RUN composer install --no-scripts --no-autoloader --no-interaction --prefer-dist

COPY app/ /var/www/html/
RUN composer dump-autoload --optimize --no-dev

# ================================
# Stage 2 — produção
# ================================
FROM php:8.3-fpm

WORKDIR /var/www/html

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN pecl install redis && docker-php-ext-enable redis
RUN docker-php-ext-install opcache

COPY infra/docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
COPY infra/docker/php/php.ini     /usr/local/etc/php/conf.d/custom.ini

COPY --from=builder /var/www/html /var/www/html

RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html \
    && chmod -R 775 /var/www/html/storage \
    && chmod -R 775 /var/www/html/bootstrap/cache

USER www-data

EXPOSE 9000
CMD ["php-fpm"]
```

## Arquivo: `infra/Dockerfile.dev`

```dockerfile
FROM php:8.3-fpm

WORKDIR /var/www/html

RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip \
    vim \
    && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN pecl install redis && docker-php-ext-enable redis

RUN docker-php-ext-install opcache

RUN pecl install xdebug && docker-php-ext-enable xdebug

COPY infra/docker/php/opcache.dev.ini /usr/local/etc/php/conf.d/opcache.ini
COPY infra/docker/php/php.ini      /usr/local/etc/php/conf.d/custom.ini
COPY infra/docker/php/xdebug.ini   /usr/local/etc/php/conf.d/xdebug.ini

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html \
    && mkdir -p /var/www/html/storage /var/www/html/bootstrap/cache \
    && chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 9000
CMD ["php-fpm"]
```

## Arquivo: `infra/.env.example`

```dotenv
# Copie para ../app/.env antes do primeiro `docker compose up`
# (o Laravel também gera .env no `composer create-project`; mantenha DB/Redis/MySQL alinhados)

APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8080

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel
DB_PASSWORD=laravel_password

MYSQL_ROOT_PASSWORD=root_password
MYSQL_DATABASE=laravel_db
MYSQL_USER=laravel
MYSQL_PASSWORD=laravel_password

REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis_password

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
```

## Arquivo: `infra/docker/php/opcache.ini`

```ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=0
opcache.validate_timestamps=0
opcache.save_comments=1```

## Arquivo: `infra/docker/php/opcache.dev.ini`

```ini
opcache.enable=0
```

## Arquivo: `infra/docker/php/php.ini`

```ini
; Memória
memory_limit = 256M

; Upload
upload_max_filesize = 100M
post_max_size = 100M

; Tempo de execução
max_execution_time = 300
max_input_time = 300

; Sessão
session.gc_maxlifetime = 3600

; Erros (desliga exibição, mantém log)
display_errors = Off
log_errors = On
error_log = /dev/stderr

; Timezone
date.timezone = America/Sao_Paulo```

## Arquivo: `infra/docker/php/xdebug.ini`

```ini
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.idekey=VSCODE
xdebug.log_level=0```

## Arquivo: `infra/nginx/default.conf`

```nginx
# Rate limiting
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=api:10m   rate=60r/m;

# Redirect HTTP → HTTPS
server {
    listen 80;
    server_name _;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    # Produção: substitua "localhost" pelo domínio real (ex.: app.exemplo.com.br).
    server_name localhost;
    root /var/www/html/public;
    index index.php index.html;

    # SSL
    ssl_certificate     /etc/nginx/certs/cert.pem;
    ssl_certificate_key /etc/nginx/certs/key.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 10m;

    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log warn;

    client_max_body_size 100M;

    # Headers de segurança
    add_header X-Frame-Options        "SAMEORIGIN"                                always;
    add_header X-Content-Type-Options "nosniff"                                   always;
    add_header X-XSS-Protection       "1; mode=block"                             always;
    add_header Referrer-Policy        "strict-origin-when-cross-origin"           always;
    add_header Permissions-Policy     "camera=(), microphone=(), geolocation=()"  always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"    always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'" always;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Rate limit em rotas de login
    location ~ ^/(login|password|register) {
        limit_req zone=login burst=10 nodelay;
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Rate limit em rotas de API
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass  app:9000;
        fastcgi_index index.php;
        include       fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_read_timeout 300;

        fastcgi_buffer_size       128k;
        fastcgi_buffers           4 256k;
        fastcgi_busy_buffers_size 256k;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}
```

## Arquivo: `infra/nginx/default.dev.conf`

```nginx
# Rate limiting
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=api:10m   rate=60r/m;

server {
    listen 80;
    server_name localhost;
    root /var/www/html/public;
    index index.php index.html;

    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log warn;

    client_max_body_size 100M;

    add_header X-Frame-Options        "SAMEORIGIN"                      always;
    add_header X-Content-Type-Options "nosniff"                         always;
    add_header X-XSS-Protection       "1; mode=block"                   always;
    add_header Referrer-Policy        "strict-origin-when-cross-origin" always;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ ^/(login|password|register) {
        limit_req zone=login burst=10 nodelay;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location /api/ {
        limit_req zone=api burst=20 nodelay;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass  app:9000;
        fastcgi_index index.php;
        include       fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_read_timeout 300;

        fastcgi_buffer_size       128k;
        fastcgi_buffers           4 256k;
        fastcgi_busy_buffers_size 256k;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}```

## Arquivo: `infra/nginx/certs/.gitkeep`

```text
(arquivo vazio — mantém pasta certs no git)
```

## Arquivo: `infra/INSTALACAO.md`

```markdown
# Instalação do Laravel com Docker

## Pré-requisitos

- Docker instalado
- Docker Compose instalado
- Estrutura com `infra/` (Docker/Nginx) e `app/` (Laravel)

---

## Ambientes

Este projeto possui dois ambientes separados:

| Ambiente | Comando (na pasta `infra/`) | Dockerfile | Nginx |
|----------|-----------------------------|------------|-------|
| Desenvolvimento | `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d` | `infra/Dockerfile.dev` | `nginx/default.dev.conf` |
| Produção | `docker compose -f docker-compose.yml up -d` | `infra/Dockerfile` | `nginx/default.conf` |

> **Build de produção (`infra/Dockerfile`):** o stage *builder* copia `app/composer.json` e `app/composer.lock`. Só faça o build da imagem de produção **depois** de o Laravel existir em `app/` (por exemplo com `composer create-project`). Com `app/` sem o projeto, o `COPY` falha — comportamento esperado.

---

## Instalação em desenvolvimento

### Passo 0 — Arquivo de ambiente do Laravel (`app/.env`)

Antes do primeiro `docker compose`, o MySQL e o Redis leem `../app/.env`. Se ainda não instalou o Laravel, copie o modelo:

```bash
cp infra/.env.example app/.env
```

Depois do `composer create-project`, o Laravel gera `app/.env`; mantenha as variáveis `DB_*`, `MYSQL_*`, `REDIS_*` e drivers alinhados com este exemplo (a imagem MySQL usa `MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD`).

### Passo 1 — Sobe MySQL e Redis

```bash
cd infra
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mysql redis
```

Aguarde os healthchecks passarem:

```bash
docker compose ps
```

Os serviços `mysql` e `redis` devem aparecer como `healthy`.

---

### Passo 2 — Instala o Laravel via Composer

Na **raiz do repositório** (pasta que contém `app/` e `infra/`):

> **Atenção:** o Composer exige que a pasta `app/` esteja **completamente vazia**.  
> Se você criou o `app/.env` no Passo 0, mova-o temporariamente para a raiz do repositório, rode o comando abaixo e depois mova de volta para `app/.env`.

```bash
docker run --rm \
  -v "$(pwd)/app:/app" \
  -w /app \
  composer:2 create-project laravel/laravel . --prefer-dist
```

> Instala o código em `app/`. No Passo 3, alinhe o `app/.env` com `DB_HOST=mysql`, `REDIS_HOST=redis`, etc. (use o `infra/.env.example` como referência).

---

### Passo 3 — Ajusta o `app/.env`

O Laravel cria o `.env` em `app/`. Garanta (como no `infra/.env.example`):

```env
APP_URL=http://localhost:8080

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel
DB_PASSWORD=laravel_password

REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis_password

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
```

---

### Passo 4 — Sobe todos os containers

```bash
cd infra
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
```

---

### Passo 5 — Gera a chave da aplicação

```bash
cd infra
docker compose exec app php artisan key:generate
```

---

### Passo 6 — Roda as migrations

```bash
docker compose exec app php artisan migrate
```

---

### Passo 7 — Ajusta as permissões

```bash
docker compose exec app chown -R www-data:www-data /var/www/html/storage
docker compose exec app chown -R www-data:www-data /var/www/html/bootstrap/cache
```

> Se estiver no **Linux** e tiver problemas para editar arquivos via VS Code (bind mount com dono `www-data` no container), rode **na máquina hospedeira** (na raiz do repositório):  
> `sudo chown -R $USER:$USER app/`

---

### Passo 8 — Acessa a aplicação

Abra no navegador:

```
http://localhost:8080
```

---

## Usando banco externo (RDS ou SQL Server)

### RDS MySQL ou RDS PostgreSQL

Remova o serviço `mysql` do `infra/docker-compose.yml` e atualize o `app/.env`:

```env
# RDS MySQL
DB_CONNECTION=mysql
DB_HOST=seu-rds.us-east-1.rds.amazonaws.com
DB_PORT=3306
DB_DATABASE=seu_banco
DB_USERNAME=seu_user
DB_PASSWORD=sua_senha
```

```env
# RDS PostgreSQL
DB_CONNECTION=pgsql
DB_HOST=seu-rds.us-east-1.rds.amazonaws.com
DB_PORT=5432
DB_DATABASE=seu_banco
DB_USERNAME=seu_user
DB_PASSWORD=sua_senha
```

> Para PostgreSQL, adicione `pdo_pgsql` no `infra/Dockerfile`:
> ```dockerfile
> && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd pdo_pgsql \
> ```
> Depois rode `docker compose build --no-cache app` (na pasta `infra/`).

---

### SQL Server (Azure / AWS RDS)

No `infra/Dockerfile`, adicione após o bloco de extensões PHP:

```dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
    unixodbc-dev \
    && pecl install sqlsrv pdo_sqlsrv \
    && docker-php-ext-enable sqlsrv pdo_sqlsrv \
    && apt-get clean && rm -rf /var/lib/apt/lists/*
```

Atualize o `.env`:

```env
DB_CONNECTION=sqlsrv
DB_HOST=seu-servidor.database.windows.net
DB_PORT=1433
DB_DATABASE=seu_banco
DB_USERNAME=seu_user
DB_PASSWORD=sua_senha
```

Depois rebuild:

```bash
cd infra
docker compose build --no-cache app
docker compose up -d
```

---


## CI/CD

Escolha a plataforma e o modo que preferir. Todas as opções fazem a mesma coisa — a diferença é só quando o deploy dispara.

---

### GitHub Actions — automático

Deploy dispara sozinho ao mergear na `main`.

Crie o arquivo `.github/workflows/deploy.yml`:

```yaml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build imagem
        run: docker build -f infra/Dockerfile -t laravel_app .

      - name: Login Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Push imagem
        run: |
          docker tag laravel_app ${{ secrets.DOCKER_USERNAME }}/laravel_app:latest
          docker push ${{ secrets.DOCKER_USERNAME }}/laravel_app:latest

      - name: Deploy no servidor
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /var/www/meu-projeto/infra
            docker compose pull
            docker compose up -d

            echo "Aguardando app ficar pronto..."
            until docker compose exec -T app php artisan --version > /dev/null 2>&1; do
              echo "  ainda subindo..."
              sleep 3
            done

            docker compose exec -T app php artisan migrate --force
            docker compose exec -T app php artisan config:cache
            docker compose exec -T app php artisan route:cache
            docker compose exec -T app php artisan view:cache
```

---

### GitHub Actions — manual

Deploy dispara só quando você clicar em "Run workflow" lá no GitHub (`Actions → Deploy → Run workflow`).

Crie o arquivo `.github/workflows/deploy.yml`:

```yaml
name: Deploy

on:
  workflow_dispatch:    # disparo manual pelo GitHub

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build imagem
        run: docker build -f infra/Dockerfile -t laravel_app .

      - name: Login Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Push imagem
        run: |
          docker tag laravel_app ${{ secrets.DOCKER_USERNAME }}/laravel_app:latest
          docker push ${{ secrets.DOCKER_USERNAME }}/laravel_app:latest

      - name: Deploy no servidor
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /var/www/meu-projeto/infra
            docker compose pull
            docker compose up -d

            echo "Aguardando app ficar pronto..."
            until docker compose exec -T app php artisan --version > /dev/null 2>&1; do
              echo "  ainda subindo..."
              sleep 3
            done

            docker compose exec -T app php artisan migrate --force
            docker compose exec -T app php artisan config:cache
            docker compose exec -T app php artisan route:cache
            docker compose exec -T app php artisan view:cache
```

Secrets necessários no GitHub (`Settings → Secrets → Actions`):

| Secret | Descrição |
|--------|-----------|
| `DOCKER_USERNAME` | Usuário do Docker Hub |
| `DOCKER_PASSWORD` | Senha do Docker Hub |
| `SERVER_HOST` | IP ou domínio do servidor |
| `SERVER_USER` | Usuário SSH do servidor |
| `SERVER_SSH_KEY` | Chave SSH privada |

> **Atenção:** a imagem Docker gerada contém o **código-fonte** da aplicação. Se o repositório de código for privado, use repositório de imagem **privado** no Docker Hub (ou outro registry) e controle quem tem `docker pull`.

---

### GitLab CI/CD — automático

Deploy dispara sozinho ao fazer push na branch `main`.

Crie o arquivo `.gitlab-ci.yml` na raiz do projeto:

```yaml
stages:
  - build
  - deploy

variables:
  IMAGE_NAME: $CI_REGISTRY_USER/laravel_app

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
  script:
    - docker build -f infra/Dockerfile -t $IMAGE_NAME:latest .
    - docker push $IMAGE_NAME:latest
  only:
    - main

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SERVER_SSH_KEY" | ssh-add -
    - mkdir -p ~/.ssh
    - ssh-keyscan $SERVER_HOST >> ~/.ssh/known_hosts
  script:
    - ssh $SERVER_USER@$SERVER_HOST "
        cd /var/www/meu-projeto/infra &&
        docker compose pull &&
        docker compose up -d &&
        echo 'Aguardando app ficar pronto...' &&
        until docker compose exec -T app php artisan --version > /dev/null 2>&1; do echo '  ainda subindo...'; sleep 3; done &&
        docker compose exec -T app php artisan migrate --force &&
        docker compose exec -T app php artisan config:cache &&
        docker compose exec -T app php artisan route:cache &&
        docker compose exec -T app php artisan view:cache
      "
  only:
    - main
```

---

### GitLab CI/CD — manual

Deploy dispara só quando você clicar em "play" lá no GitLab (`CI/CD → Pipelines → play no job deploy`).

Crie o arquivo `.gitlab-ci.yml` na raiz do projeto:

```yaml
stages:
  - build
  - deploy

variables:
  IMAGE_NAME: $CI_REGISTRY_USER/laravel_app

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
  script:
    - docker build -f infra/Dockerfile -t $IMAGE_NAME:latest .
    - docker push $IMAGE_NAME:latest
  only:
    - main

deploy:
  stage: deploy
  image: alpine:latest
  when: manual        # disparo manual
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SERVER_SSH_KEY" | ssh-add -
    - mkdir -p ~/.ssh
    - ssh-keyscan $SERVER_HOST >> ~/.ssh/known_hosts
  script:
    - ssh $SERVER_USER@$SERVER_HOST "
        cd /var/www/meu-projeto/infra &&
        docker compose pull &&
        docker compose up -d &&
        echo 'Aguardando app ficar pronto...' &&
        until docker compose exec -T app php artisan --version > /dev/null 2>&1; do echo '  ainda subindo...'; sleep 3; done &&
        docker compose exec -T app php artisan migrate --force &&
        docker compose exec -T app php artisan config:cache &&
        docker compose exec -T app php artisan route:cache &&
        docker compose exec -T app php artisan view:cache
      "
  only:
    - main
```

Variáveis necessárias no GitLab (`Settings → CI/CD → Variables`):

| Variável | Descrição |
|----------|-----------|
| `CI_REGISTRY_USER` | Usuário do Docker Hub |
| `CI_REGISTRY_PASSWORD` | Senha do Docker Hub |
| `SERVER_HOST` | IP ou domínio do servidor |
| `SERVER_USER` | Usuário SSH do servidor |
| `SERVER_SSH_KEY` | Chave SSH privada |

> **Atenção:** a imagem Docker gerada contém o **código-fonte** da aplicação. Se o repositório de código for privado, use repositório de imagem **privado** no Docker Hub (ou outro registry) e controle quem tem `docker pull`.

---

## HTTPS com Let's Encrypt (Certbot)

Em produção, use Certbot para gerar certificados gratuitos. No servidor:

### Passo 1 — Instala o Certbot

```bash
apt install certbot python3-certbot-nginx -y
```

### Passo 2 — Gera o certificado

```bash
certbot certonly --standalone -d seu-dominio.com.br
```

Os certificados são gerados em:
```
/etc/letsencrypt/live/seu-dominio.com.br/fullchain.pem
/etc/letsencrypt/live/seu-dominio.com.br/privkey.pem
```

> **Atenção:** o Nginx só sobe se os arquivos `cert.pem` e `key.pem` já existirem nos caminhos mapeados (ex.: `./nginx/certs/`). Garanta que o Certbot (ou outra origem) tenha gerado/copiado os certificados **antes** de subir o stack com os volumes de SSL no `docker-compose.yml`.

### Passo 3 — Mapeia os certificados no `infra/docker-compose.yml`

```yaml
nginx:
  volumes:
    - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    - /etc/letsencrypt/live/seu-dominio.com.br/fullchain.pem:/etc/nginx/certs/cert.pem:ro
    - /etc/letsencrypt/live/seu-dominio.com.br/privkey.pem:/etc/nginx/certs/key.pem:ro
```

### Passo 4 — Renovação automática

O Certbot já instala um cron de renovação automática. Para testar:

```bash
certbot renew --dry-run
```

---

## Monitoramento

### Opção simples — healthcheck endpoint

Adicione em `routes/web.php`:

```php
Route::get('/health', function () {
    return response()->json([
        'status' => 'ok',
        'timestamp' => now(),
    ]);
});
```

Monitore com UptimeRobot ou Better Uptime (ambos gratuitos) apontando para `https://seu-dominio.com.br/health`.

---

### Opção avançada — Prometheus + Grafana

Adicione ao `infra/docker-compose.yml` (e crie `infra/docker/prometheus/prometheus.yml`):

```yaml
  prometheus:
    image: prom/prometheus:latest
    container_name: laravel_prometheus
    volumes:
      - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    ports:
      - "127.0.0.1:9090:9090"
    networks:
      - laravel_network

  grafana:
    image: grafana/grafana:latest
    container_name: laravel_grafana
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - laravel_network

volumes:
  grafana_data:
```

Crie `docker/prometheus/prometheus.yml`:

```yaml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'laravel'
    static_configs:
      - targets: ['app:9000']
```

Acesse o Grafana em `http://localhost:3000` (usuário: `admin`, senha: `admin`).

---

## Comandos úteis do dia a dia

| Ação | Comando (com `cd infra`) |
|------|--------------------------|
| Ver logs | `docker compose logs -f` |
| Ver logs de um serviço | `docker compose logs -f app` |
| Entrar no container | `docker compose exec app bash` |
| Rodar artisan | `docker compose exec app php artisan <comando>` |
| Parar tudo | `docker compose down` |
| Parar e apagar volumes | `docker compose down -v` |
| Rebuild da imagem | `docker compose build --no-cache app` |
| Limpar todos os caches | `docker compose exec app php artisan optimize:clear` |

---

## Troubleshooting

### Container `app` não sobe

Verifique os logs:
```bash
docker compose logs app
```

Causas comuns:
- MySQL ou Redis ainda não estão `healthy` — aguarde e tente novamente
- Erro de permissão no `storage/` — rode:
```bash
docker compose exec app chmod -R 775 /var/www/html/storage
docker compose exec app chmod -R 775 /var/www/html/bootstrap/cache
```

---

### Erro 502 Bad Gateway no Nginx

O Nginx não consegue se comunicar com o PHP-FPM. Verifique:
```bash
docker compose logs nginx
docker compose logs app
```

Causas comuns:
- Container `app` não está rodando — `docker compose up -d app`
- `fastcgi_pass app:9000` com nome errado — confirma que o serviço no `infra/docker-compose.yml` se chama `app`

---

### Erro de permissão no `storage/`

```bash
docker compose exec app bash -c "chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache"
docker compose exec app bash -c "chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache"
```

---

### MySQL não conecta

Verifique se o healthcheck passou:
```bash
docker compose ps
```

Se aparecer `starting` ao invés de `healthy`, aguarde ou verifique os logs:
```bash
docker compose logs mysql
```

Confirme que `app/.env` contém `DB_*`, `MYSQL_*` e `REDIS_PASSWORD` coerentes com o que os serviços esperam.

---

### Limpar tudo e recomeçar

```bash
docker compose down -v        # para containers e apaga volumes
docker compose build --no-cache app  # rebuild da imagem
docker compose up -d          # sobe tudo do zero
```

> **Atenção:** `down -v` apaga os dados do banco. Use só em ambiente local.

---

## Estrutura do repositório

- **`app/`** — código Laravel (`.env` aqui, não versionado; `composer create-project` cria `public/`, `storage/`, etc.).
- **`infra/`** — Docker, Nginx e documentação; subir os containers sempre a partir de `infra/` (ou use `-f infra/docker-compose.yml` na raiz).

O **contexto de build** da imagem é a **raiz do repositório**; o arquivo **`.dockerignore`** fica na raiz (ao lado de `app/` e `infra/`), não dentro de `infra/`.

```
meu-projeto/
├── .dockerignore
├── .gitignore
├── app/
│   ├── .env              ← não versionado (Laravel + variáveis usadas pelo Compose)
│   └── .gitignore
└── infra/
    ├── docker/
    │   └── php/
    │       ├── opcache.ini
    │       ├── opcache.dev.ini
    │       ├── php.ini
    │       └── xdebug.ini
    ├── nginx/
    │   ├── certs/
    │   │   └── .gitkeep
    │   ├── default.conf
    │   └── default.dev.conf
    ├── docker-compose.yml
    ├── docker-compose.dev.yml
    ├── Dockerfile
    ├── Dockerfile.dev
    ├── .env.example        ← modelo para copiar em ../app/.env antes do primeiro up
    └── INSTALACAO.md
```

```
