Se vocΓͺ ainda faz deploy da sua API Laravel conectando no servidor via SSH, rodando git pull e rezando para que nada quebre, este artigo pode ser ΓΊtil para ti.
Vou mostrar como configurar um de CI/CD completo no GitLab, do zero, para uma aplicaΓ§Γ£o Laravel hospedada em uma VPS comum. Ao final, cada push na branch certa vai automaticamente instalar dependΓͺncias, verificar a qualidade do cΓ³digo, rodar os testes, construir uma imagem Docker e fazer deploy no servidor, sem nenhuma intervenΓ§Γ£o manual.
O que vamos construir
Um pipeline com 5 stages em sequΓͺncia:
prepare β quality β test β build β deploy
| Stage | O que faz |
|---|---|
prepare |
Instala dependΓͺncias com Composer |
quality |
Verifica o estilo do cΓ³digo com Laravel Pint |
test |
Executa os testes com Pest |
build |
ConstrΓ³i a imagem Docker e publica no Registry |
deploy |
Faz o deploy na VPS via SSH |
PrΓ©-requisitos
- Uma conta no GitLab (gitlab.com ou self-hosted)
- Uma VPS com Ubuntu/Debian e Docker instalado
- Um projeto Laravel com Pest configurado
- Acesso SSH Γ VPS
Parte 1: O GitLab Runner
O GitLab por si sΓ³ nΓ£o executa nenhum comando. Quem faz o trabalho pesado Γ© o GitLab Runner: um agente leve que vocΓͺ instala em qualquer mΓ‘quina (sua VPS, um servidor dedicado, ou atΓ© o prΓ³prio computador) e que fica escutando o GitLab por jobs para executar.
Como o Runner funciona
ββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββ
β GitLab βββpollingββ GitLab Runner βββexecutaββΊβ Seu cΓ³digo β
β (servidor) β β (na sua VPS) β β β
ββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββ
O Runner faz polling no GitLab a cada poucos segundos. Quando hΓ‘ um job disponΓvel, ele pega, executa e devolve o resultado. VocΓͺ pode ter quantos runners quiser, em mΓ‘quinas diferentes, e o GitLab distribui os jobs entre eles.
Tipos de executor
O Runner suporta vΓ‘rios executores. Os dois mais usados sΓ£o:
| Executor | Como funciona | Quando usar |
|---|---|---|
| Shell | Executa comandos diretamente no servidor | Simples, sem isolamento |
| Docker | Executa cada job dentro de um container | Recomendado β isolamento total |
Usaremos o executor Docker: cada job roda em um container descartΓ‘vel com a imagem que vocΓͺ especificar. Isso garante que o ambiente Γ© sempre limpo e reproduzΓvel.
Instalando o GitLab Runner na VPS
Conecte na sua VPS via SSH e execute:
# Adiciona o repositΓ³rio oficial do GitLab Runner
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
# Instala o Runner
sudo apt-get install -y gitlab-runner
# Verifica se o serviΓ§o estΓ‘ rodando
sudo systemctl status gitlab-runner
π‘ O Runner roda como um serviΓ§o do sistema (
systemd) e Γ© iniciado automaticamente com o servidor.
Registrando o Runner no GitLab
Com o Runner instalado, vocΓͺ precisa registrΓ‘-lo no seu projeto do GitLab. Γ nesta etapa que vocΓͺ associa o agente ao repositΓ³rio.
No GitLab, vΓ‘ em: Settings > CI/CD > Runners > New project runner
O GitLab vai gerar um token de registro. De volta Γ VPS, execute:
sudo gitlab-runner register
O comando vai fazer algumas perguntas interativas:
Enter the GitLab instance URL:
> https://gitlab.com
Enter the registration token:
> glrt-xxxxxxxxxxxxxxxxxxxx
Enter a description for the runner:
> vps-production-runner
Enter tags for the runner (comma-separated):
> docker,laravel
Enter optional maintenance note for the runner:
>
Enter an executor:
> docker
Enter the default Docker image:
> php:8.4-cli-bookworm
ApΓ³s o registro, o Runner aparece na interface do GitLab como "Online".
ConfiguraΓ§Γ£o adicional para Docker-in-Docker
Para que os jobs de build do Docker funcionem dentro de containers, vocΓͺ precisa de uma configuraΓ§Γ£o extra. Edite o arquivo /etc/gitlab-runner/config.toml:
[[runners]]
name = "vps-production-runner"
url = "https://gitlab.com"
token = "glrt-xxxxxxxxxxxxxxxxxxxx"
executor = "docker"
[runners.docker]
image = "php:8.4-cli-bookworm"
privileged = true # necessΓ‘rio para Docker-in-Docker
volumes = [
"/certs/client", # certificados TLS para DinD
"/cache"
]
O modo privileged = true Γ© necessΓ‘rio para que o container do job possa acessar o daemon Docker do host durante o build da imagem.
ApΓ³s editar, reinicie o Runner:
sudo systemctl restart gitlab-runner
β οΈ SeguranΓ§a: use
privileged = trueapenas nos runners dedicados ao build de imagens Docker. Para runners de testes, mantenhaprivileged = false.
Parte 2: O arquivo .gitlab-ci.yml
Toda a lΓ³gica do pipeline vive em um ΓΊnico arquivo na raiz do projeto: .gitlab-ci.yml. O GitLab lΓͺ esse arquivo a cada push e monta o pipeline correspondente.
Vamos construir o arquivo completo, seΓ§Γ£o por seΓ§Γ£o.
ConfiguraΓ§Γ΅es globais
# .gitlab-ci.yml
default:
interruptible: true
retry:
max: 1
when:
- runner_system_failure
- stuck_or_timeout_failure
workflow:
rules:
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH =~ /^feature\//
- when: never
Explicando cada decisΓ£o:
-
interruptible: trueβ se um novo commit chegar enquanto o pipeline estΓ‘ rodando, o pipeline antigo Γ© cancelado. Economiza recursos do runner e evita deploys fora de ordem. -
retryβ o pipeline tenta novamente em caso de falhas de infraestrutura (runner caiu, timeout), e nΓ£o por falhas do seu cΓ³digo. -
workflow.rulesβ define quais situaΓ§Γ΅es criam um pipeline. Branches que nΓ£o estΓ£o listadas (comochore/fix-typo) nΓ£o geram pipeline algum. Owhen: neverno final age como um "else" que descarta tudo que nΓ£o foi explicitamente listado.
Stages e variΓ‘veis
stages:
- prepare
- quality
- test
- build
- deploy
variables:
PHP_VERSION: "8.4"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
COMPOSER_ALLOW_SUPERUSER: "1"
COMPOSER_NO_INTERACTION: "1"
A ordem dos stages define a sequΓͺncia de execuΓ§Γ£o. Um stage sΓ³ comeΓ§a quando todos os jobs do stage anterior passam. As variΓ‘veis configuram o ambiente de forma consistente para todos os jobs.
COMPOSER_CACHE_DIR aponta o cache do Composer para dentro do projeto. Isso permite configurar cache entre pipelines mais facilmente (veremos a seguir).
Templates reutilizΓ‘veis
# Template base para todos os jobs PHP
.php-env:
image: "php:${PHP_VERSION}-cli-bookworm"
before_script:
- apt-get update -qq && apt-get install -y -qq git unzip libzip-dev libsqlite3-dev
- docker-php-ext-install zip pdo_sqlite
# Γncoras de regras para nΓ£o repetir em cada job
.rules-ci: &rules-ci
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH =~ /^feature\//
.rules-deploy: &rules-deploy
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
Dois recursos poderosos do GitLab CI aqui:
-
Job templates (prefixo
.): nΓ£o geram jobs reais, servem apenas como base para outros jobs viaextends. Γ como heranΓ§a, vocΓͺ define uma vez e reutiliza em vΓ‘rios lugares. -
Γncoras YAML (
&define,*referencia): evitam repetir as mesmas regras em cada job. Se precisar adicionar uma branch, edita em um ΓΊnico lugar.
Parte 3: Os jobs do pipeline
Stage prepare: instalando dependΓͺncias
composer:install:
extends: .php-env
stage: prepare
script:
- cp .env.example .env
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --prefer-dist --no-progress --no-interaction
- php artisan key:generate --ansi
artifacts:
paths:
- vendor/
- .env
expire_in: 2 hours
cache:
key: composer-$CI_COMMIT_REF_SLUG
paths:
- .composer-cache/
rules: *rules-ci
Este job Γ© a base de tudo. Ele instala as dependΓͺncias e as disponibiliza para todos os jobs seguintes.
O mecanismo de artifacts Γ© o que elimina a necessidade de reinstalar dependΓͺncias em cada job:
- Os diretΓ³rios
vendor/e o arquivo.envsΓ£o "empacotados" ao final do job e armazenados temporariamente no GitLab. - Qualquer job que declare
needs: [{job: composer:install, artifacts: true}]recebe esses arquivos automaticamente antes de executar, sem baixar nada novamente. -
expire_in: 2 hoursgarante limpeza automΓ‘tica.
O cache Γ© diferente dos artifacts: ele persiste entre pipelines diferentes (nΓ£o apenas entre jobs do mesmo pipeline). Isso acelera builds subsequentes porque o Composer nΓ£o precisa baixar os pacotes da internet toda vez, apenas verifica se algo mudou no composer.lock.
Stage quality: verificaΓ§Γ£o de estilo com Pint
pint:
extends: .php-env
stage: quality
needs:
- job: composer:install
artifacts: true
script:
- vendor/bin/pint --test
rules: *rules-ci
O Laravel Pint Γ© o formatador de cΓ³digo oficial do Laravel, baseado no PHP-CS-Fixer. O flag --test Γ© a chave: ele nΓ£o modifica nenhum arquivo, apenas verifica se o cΓ³digo estΓ‘ em conformidade com as regras configuradas no pint.json (ou usa o preset laravel por padrΓ£o).
Se algum arquivo estiver mal formatado, o Pint retorna um cΓ³digo de saΓda diferente de zero, o job falha e o pipeline para, antes mesmo de rodar os testes. Isso dΓ‘ feedback rΓ‘pido ao desenvolvedor.
π‘ Configure seu editor para rodar o Pint ao salvar o arquivo. O CI Γ© a rede de seguranΓ§a, nΓ£o o seu fluxo de trabalho principal.
Stage test: testes automatizados com Pest
pest:
extends: .php-env
stage: test
needs:
- job: composer:install
artifacts: true
script:
- php artisan test --compact
rules: *rules-ci
O Pest Γ© um framework de testes elegante para PHP, construΓdo sobre o PHPUnit. O comando php artisan test Γ© um wrapper conveniente do Laravel.
O flag --compact exibe os resultados de forma condensada, perfeito para os logs do CI onde vocΓͺ quer identificar rapidamente o que passou e o que falhou.
Sobre o needs: com ele vocΓͺ define dependΓͺncias entre jobs individuais, e nΓ£o entre stages inteiros. Isso significa que pint e pest podem rodar em paralelo (ambos dependem apenas de composer:install), acelerando o pipeline. O stage test sΓ³ comeΓ§a quando o stage quality termina, mas dentro do mesmo stage, jobs sem dependΓͺncias entre si rodam simultaneamente.
Stage build: construindo e publicando a imagem Docker
.docker-publish:
stage: build
image: docker:27.4.0-cli
services:
- docker:27.4.0-dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
script:
- |
export IMAGE="${CI_REGISTRY_IMAGE}:${DOCKER_IMAGE_TAG}"
export IMAGE_SHA="${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
docker build -t "${IMAGE}" -t "${IMAGE_SHA}" .
docker push "${IMAGE}"
docker push "${IMAGE_SHA}"
echo "β Imagem publicada: ${IMAGE}"
docker:build:develop:
extends: .docker-publish
variables:
DOCKER_IMAGE_TAG: develop
needs:
- job: pest
rules:
- if: $CI_COMMIT_BRANCH == "develop"
docker:build:main:
extends: .docker-publish
variables:
DOCKER_IMAGE_TAG: latest
needs:
- job: pest
rules:
- if: $CI_COMMIT_BRANCH == "main"
docker:build:release:
stage: build
image: docker:27.4.0-cli
services:
- docker:27.4.0-dind
needs:
- job: pest
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
script:
- |
export RELEASE_VERSION="${CI_COMMIT_TAG#v}"
export IMAGE="${CI_REGISTRY_IMAGE}:${RELEASE_VERSION}"
export IMAGE_SHA="${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
docker build -t "${IMAGE}" -t "${CI_REGISTRY_IMAGE}:latest" -t "${IMAGE_SHA}" .
docker push "${IMAGE}"
docker push "${CI_REGISTRY_IMAGE}:latest"
docker push "${IMAGE_SHA}"
echo "β Release ${RELEASE_VERSION} publicada"
rules:
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
Entendendo as variΓ‘veis automΓ‘ticas do GitLab:
| VariΓ‘vel | O que contΓ©m |
|---|---|
$CI_REGISTRY |
URL do GitLab Container Registry |
$CI_REGISTRY_USER |
UsuΓ‘rio para autenticaΓ§Γ£o (automΓ‘tico) |
$CI_REGISTRY_PASSWORD |
Senha para autenticaΓ§Γ£o (automΓ‘tico) |
$CI_REGISTRY_IMAGE |
Caminho completo da imagem (ex: registry.gitlab.com/grupo/projeto) |
$CI_COMMIT_SHORT_SHA |
Primeiros 8 caracteres do hash do commit |
$CI_COMMIT_TAG |
A tag Git que disparou o pipeline (ex: v1.4.2) |
Por que tagear com o SHA do commit?
registry.gitlab.com/seu-grupo/api:latest β versΓ£o atual de produΓ§Γ£o
registry.gitlab.com/seu-grupo/api:1.4.2 β versΓ£o semΓ’ntica
registry.gitlab.com/seu-grupo/api:a1b2c3d4 β commit exato
Com a tag do SHA, vocΓͺ sempre sabe qual commit gerou qual imagem. Isso Γ© fundamental para rastreabilidade e para fazer rollback: basta saber o SHA do commit anterior e trocar a tag no servidor.
O serviΓ§o docker:27.4.0-dind (Docker-in-Docker) permite executar comandos Docker dentro de um container de CI. Γ por isso que o executor do Runner precisa de privileged = true.
Stage deploy: fazendo deploy na VPS via SSH
Este Γ© o estΓ‘gio final e onde a mΓ‘gica acontece. O job conecta na sua VPS via SSH e atualiza o container em execuΓ§Γ£o.
.deploy-ssh:
stage: deploy
image: alpine:3.21
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
deploy:develop:
extends: .deploy-ssh
script:
- |
ssh $SSH_USER@$SSH_HOST_DEVELOP "
docker pull ${CI_REGISTRY_IMAGE}:develop
docker stop laravel-api-develop || true
docker rm laravel-api-develop || true
docker run -d \
--name laravel-api-develop \
--restart unless-stopped \
-p 8081:80 \
--env-file /opt/apps/api-develop/.env \
${CI_REGISTRY_IMAGE}:develop
docker image prune -f
"
environment:
name: develop
url: $DEPLOY_DEVELOP_URL
needs:
- job: docker:build:develop
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy:production:
extends: .deploy-ssh
script:
- |
ssh $SSH_USER@$SSH_HOST_PRODUCTION "
docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
docker pull ${CI_REGISTRY_IMAGE}:latest
docker stop laravel-api || true
docker rm laravel-api || true
docker run -d \
--name laravel-api \
--restart unless-stopped \
-p 8080:80 \
--env-file /opt/apps/api/.env \
${CI_REGISTRY_IMAGE}:latest
docker image prune -f
"
environment:
name: production
url: $DEPLOY_PRODUCTION_URL
when: manual
needs:
- job: docker:build:main
rules:
- if: $CI_COMMIT_BRANCH == "main"
Analisando o script de deploy:
-
docker pullβ baixa a imagem mais recente do registry para o servidor. -
docker stop+docker rmβ para e remove o container anterior. O|| trueevita que o script falhe se o container nΓ£o existir (primeira execuΓ§Γ£o). -
docker runβ sobe o novo container com as configuraΓ§Γ΅es de produΓ§Γ£o. O--env-fileaponta para um arquivo.envque vocΓͺ mantΓ©m manualmente no servidor, nunca commite credenciais de produΓ§Γ£o no repositΓ³rio. -
docker image prune -fβ limpa imagens antigas nΓ£o utilizadas para evitar que o disco da VPS se esgote.
when: manual no deploy de produΓ§Γ£o Γ© uma decisΓ£o intencional de seguranΓ§a: o job de deploy para produΓ§Γ£o nΓ£o executa automaticamente. Ele fica disponΓvel na interface do GitLab aguardando a aprovaΓ§Γ£o humana. Um desenvolvedor ou tech lead precisa clicar em "Play" para disparar o deploy.
Parte 4: Configurando as chaves SSH
Para o deploy funcionar, o Runner precisa se autenticar na VPS via SSH sem senha. Vamos configurar isso de forma segura usando variΓ‘veis do GitLab.
Gerando o par de chaves
Na sua mΓ‘quina local (ou em qualquer lugar seguro), gere um par de chaves dedicado para o CI:
ssh-keygen -t ed25519 -C "gitlab-ci-deploy" -f ~/.ssh/gitlab_ci_deploy -N ""
Isso gera dois arquivos:
-
~/.ssh/gitlab_ci_deployβ a chave privada (vai para o GitLab) -
~/.ssh/gitlab_ci_deploy.pubβ a chave pΓΊblica (vai para o servidor)
Adicionando a chave pΓΊblica ao servidor
# Copie o conteΓΊdo da chave pΓΊblica
cat ~/.ssh/gitlab_ci_deploy.pub
# No servidor, adicione ao arquivo authorized_keys do usuΓ‘rio de deploy
echo "CONTEUDO_DA_CHAVE_PUBLICA" >> ~/.ssh/authorized_keys
Obtendo o Known Hosts
# Na sua mΓ‘quina local, obtenha a assinatura do servidor
ssh-keyscan -H SEU_IP_DA_VPS
Copie a saΓda (vai parecer algo como |1|abc123...|ssh-ed25519 AAAA...).
Configurando as variΓ‘veis no GitLab
VΓ‘ em Settings > CI/CD > Variables e adicione:
| VariΓ‘vel | Valor | Tipo | Protegida |
|---|---|---|---|
SSH_PRIVATE_KEY |
ConteΓΊdo de gitlab_ci_deploy
|
File | β Sim |
SSH_KNOWN_HOSTS |
SaΓda do ssh-keyscan
|
Variable | β Sim |
SSH_USER |
UsuΓ‘rio SSH da VPS (ex: ubuntu) |
Variable | β Sim |
SSH_HOST_PRODUCTION |
IP ou hostname da VPS | Variable | β Sim |
SSH_HOST_DEVELOP |
IP ou hostname do servidor de dev | Variable | β Sim |
DEPLOY_PRODUCTION_URL |
URL da API em produΓ§Γ£o | Variable | NΓ£o |
DEPLOY_DEVELOP_URL |
URL do ambiente de dev | Variable | NΓ£o |
β οΈ Marque todas as variΓ‘veis sensΓveis como Masked e Protected. Masked impede que o valor apareΓ§a nos logs. Protected restringe o uso a branches e tags protegidas.
Parte 5: O Dockerfile multi-stage
O Dockerfile usa o padrΓ£o multi-stage build, essencial para imagens de produΓ§Γ£o enxutas e seguras:
# syntax=docker/dockerfile:1
# ββββββββββββββββββββββββββββββββββββββββββββββ
# EstΓ‘gio 1: instala dependΓͺncias via Composer
# ββββββββββββββββββββββββββββββββββββββββββββββ
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-interaction \
--no-scripts \
--prefer-dist \
--optimize-autoloader
COPY app ./app
COPY bootstrap ./bootstrap
COPY config ./config
COPY database ./database
COPY routes ./routes
COPY artisan ./artisan
RUN composer dump-autoload --optimize --classmap-authoritative --no-dev
# ββββββββββββββββββββββββββββββββββββββββββββββ
# EstΓ‘gio 2: imagem de runtime final
# ββββββββββββββββββββββββββββββββββββββββββββββ
FROM php:8.4-fpm-bookworm AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
nginx supervisor libzip-dev libpng-dev \
libonig-dev libxml2-dev libpq-dev curl \
&& docker-php-ext-install bcmath opcache pdo_mysql pdo_pgsql zip \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /var/www/html
# Copia apenas o vendor compilado do estΓ‘gio anterior
COPY --from=vendor /app/vendor ./vendor
COPY app ./app
COPY bootstrap ./bootstrap
COPY config ./config
COPY database ./database
COPY public ./public
COPY resources ./resources
COPY routes ./routes
COPY artisan ./artisan
COPY docker/nginx/default.conf /etc/nginx/sites-available/default
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/php/php.ini /usr/local/etc/php/conf.d/php.ini
RUN mkdir -p storage/framework/{cache,sessions,views} storage/logs bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R ug+rwx storage bootstrap/cache
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Por que dois estΓ‘gios?
- O estΓ‘gio
vendorusa a imagem oficial do Composer (que inclui git, unzip e tudo mais necessΓ‘rio para instalar pacotes PHP) e gera a pastavendor/otimizada. - O estΓ‘gio
runtimecomeΓ§a do zero com uma imagem PHP limpa e copia apenas ovendor/jΓ‘ pronto. - A imagem final nΓ£o contΓ©m o Composer, git, ou qualquer outra ferramenta de build. Isso reduz o tamanho da imagem e a superfΓcie de ataque em produΓ§Γ£o.
O Supervisor gerencia dois processos dentro do mesmo container: o PHP-FPM (processa as requisiΓ§Γ΅es PHP) e o Nginx (recebe as requisiΓ§Γ΅es HTTP e as repassa ao PHP-FPM). Para uma API stateless, esta Γ© uma abordagem simples e eficaz.
Parte 6: Preparando a VPS para o deploy
Estrutura de diretΓ³rios
Crie a estrutura de diretΓ³rios no servidor:
sudo mkdir -p /opt/apps/api
sudo chown -R $USER:$USER /opt/apps/api
Arquivo .env de produΓ§Γ£o
Crie o arquivo .env de produΓ§Γ£o diretamente no servidor:
nano /opt/apps/api/.env
APP_NAME="Minha API"
APP_ENV=production
APP_KEY=base64:SUA_APP_KEY_AQUI
APP_DEBUG=false
APP_URL=https://api.meudominio.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=minha_api_prod
DB_USERNAME=usuario_prod
DB_PASSWORD=senha_super_segura
# ... demais variΓ‘veis
π Este arquivo nunca deve ir para o repositΓ³rio. Ele contΓ©m segredos de produΓ§Γ£o e deve existir apenas no servidor.
Autenticando o Docker no Registry
No servidor de produΓ§Γ£o, autentique o Docker no GitLab Registry para que ele consiga fazer docker pull da imagem privada:
docker login registry.gitlab.com
Ou, para automatizar sem senha interativa, crie um Deploy Token no GitLab (Settings > Repository > Deploy tokens) com permissΓ£o read_registry e use:
docker login registry.gitlab.com -u SEU_DEPLOY_TOKEN_USER -p SEU_DEPLOY_TOKEN_PASSWORD
O Docker salva as credenciais em ~/.docker/config.json, entΓ£o futuras chamadas docker pull funcionam sem autenticaΓ§Γ£o manual.
O arquivo .gitlab-ci.yml completo
# .gitlab-ci.yml
default:
interruptible: true
retry:
max: 1
when:
- runner_system_failure
- stuck_or_timeout_failure
workflow:
rules:
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH =~ /^feature\//
- when: never
stages:
- prepare
- quality
- test
- build
- deploy
variables:
PHP_VERSION: "8.4"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
COMPOSER_ALLOW_SUPERUSER: "1"
COMPOSER_NO_INTERACTION: "1"
# ββ Templates ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
.php-env:
image: "php:${PHP_VERSION}-cli-bookworm"
before_script:
- apt-get update -qq && apt-get install -y -qq git unzip libzip-dev libsqlite3-dev
- docker-php-ext-install zip pdo_sqlite
.rules-ci: &rules-ci
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH =~ /^feature\//
.deploy-ssh:
stage: deploy
image: alpine:3.21
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
# ββ Stage: prepare ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
composer:install:
extends: .php-env
stage: prepare
script:
- cp .env.example .env
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --prefer-dist --no-progress --no-interaction
- php artisan key:generate --ansi
artifacts:
paths:
- vendor/
- .env
expire_in: 2 hours
cache:
key: composer-$CI_COMMIT_REF_SLUG
paths:
- .composer-cache/
rules: *rules-ci
# ββ Stage: quality ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
pint:
extends: .php-env
stage: quality
needs:
- job: composer:install
artifacts: true
script:
- vendor/bin/pint --test
rules: *rules-ci
# ββ Stage: test βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
pest:
extends: .php-env
stage: test
needs:
- job: composer:install
artifacts: true
script:
- php artisan test --compact
rules: *rules-ci
# ββ Stage: build ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
.docker-publish:
stage: build
image: docker:27.4.0-cli
services:
- docker:27.4.0-dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
script:
- |
export IMAGE="${CI_REGISTRY_IMAGE}:${DOCKER_IMAGE_TAG}"
export IMAGE_SHA="${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
docker build -t "${IMAGE}" -t "${IMAGE_SHA}" .
docker push "${IMAGE}"
docker push "${IMAGE_SHA}"
echo "β Publicado: ${IMAGE}"
docker:build:develop:
extends: .docker-publish
variables:
DOCKER_IMAGE_TAG: develop
needs:
- job: pest
rules:
- if: $CI_COMMIT_BRANCH == "develop"
docker:build:main:
extends: .docker-publish
variables:
DOCKER_IMAGE_TAG: latest
needs:
- job: pest
rules:
- if: $CI_COMMIT_BRANCH == "main"
docker:build:release:
stage: build
image: docker:27.4.0-cli
services:
- docker:27.4.0-dind
needs:
- job: pest
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
script:
- |
export RELEASE_VERSION="${CI_COMMIT_TAG#v}"
export IMAGE="${CI_REGISTRY_IMAGE}:${RELEASE_VERSION}"
docker build -t "${IMAGE}" -t "${CI_REGISTRY_IMAGE}:latest" -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}" .
docker push "${IMAGE}"
docker push "${CI_REGISTRY_IMAGE}:latest"
docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
echo "β Release ${RELEASE_VERSION} publicada"
rules:
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
# ββ Stage: deploy βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
deploy:develop:
extends: .deploy-ssh
script:
- |
ssh $SSH_USER@$SSH_HOST_DEVELOP "
docker pull ${CI_REGISTRY_IMAGE}:develop
docker stop laravel-api-develop || true
docker rm laravel-api-develop || true
docker run -d \
--name laravel-api-develop \
--restart unless-stopped \
-p 8081:80 \
--env-file /opt/apps/api-develop/.env \
${CI_REGISTRY_IMAGE}:develop
docker image prune -f
"
environment:
name: develop
url: $DEPLOY_DEVELOP_URL
needs:
- job: docker:build:develop
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy:production:
extends: .deploy-ssh
script:
- |
ssh $SSH_USER@$SSH_HOST_PRODUCTION "
docker pull ${CI_REGISTRY_IMAGE}:latest
docker stop laravel-api || true
docker rm laravel-api || true
docker run -d \
--name laravel-api \
--restart unless-stopped \
-p 8080:80 \
--env-file /opt/apps/api/.env \
${CI_REGISTRY_IMAGE}:latest
docker image prune -f
"
environment:
name: production
url: $DEPLOY_PRODUCTION_URL
when: manual
needs:
- job: docker:build:main
rules:
- if: $CI_COMMIT_BRANCH == "main"
O fluxo completo em um diagrama
Push na branch main
β
βΌ
ββββββββββββββββββββββ
β composer:install β β instala deps, gera artefato vendor/ + .env
ββββββββββββ¬ββββββββββ
β artifacts
ββββββ΄βββββ
βΌ βΌ
βββββββββ ββββββββ
β pint β β pest β β rodam em paralelo (dependem apenas de composer:install)
βββββββββ ββββ¬ββββ
β pass
βΌ
ββββββββββββββββββββββ
β docker:build:main β β build + push :latest e :sha
ββββββββββββ¬ββββββββββ
β
βΌ
βββββββββββββββββββββ
β deploy:productionβ β aguarda clique manual [βΆ Play]
β SSH β VPS β
β docker pull β
β docker run β
βββββββββββββββββββββ
Checklist de configuraΓ§Γ£o
Antes de testar o pipeline pela primeira vez, verifique:
- [ ] GitLab Runner instalado e registrado no projeto
- [ ] Executor configurado como
dockercomprivileged = true - [ ] Par de chaves SSH gerado e configurado
- [ ] Chave pΓΊblica adicionada ao
authorized_keysda VPS - [ ] VariΓ‘veis de CI/CD configuradas no GitLab
- [ ] Arquivo
.envde produΓ§Γ£o criado manualmente na VPS - [ ] Docker autenticado no GitLab Registry na VPS
- [ ] DiretΓ³rio
/opt/apps/apicriado na VPS - [ ]
pint.jsonconfigurado no projeto (ou usando o preset padrΓ£olaravel) - [ ] Testes Pest passando localmente antes do primeiro push
ConclusΓ£o
Com essa configuraΓ§Γ£o, cada push no seu repositΓ³rio dispara um pipeline que garante automaticamente que o cΓ³digo estΓ‘ bem formatado, os testes passam e, quando vocΓͺ quiser, a nova versΓ£o Γ© entregue ao servidor com um ΓΊnico clique.
O resultado Γ© confianΓ§a para deployar. NΓ£o mais SSH manual, nΓ£o mais "serΓ‘ que quebrou alguma coisa?", nΓ£o mais deploy Γ s sextas Γ s 17h com o coraΓ§Γ£o na mΓ£o.
Esta estrutura Γ© simples o suficiente para um time de uma pessoa e escalΓ‘vel para crescer com o projeto: vocΓͺ pode adicionar novos ambientes, novos stages de anΓ‘lise estΓ‘tica (PHPStan, por exemplo) ou migrar para um orquestrador mais sofisticado depois β sem precisar reescrever tudo.













