Aşağıdaki kılavuz, Laravel uygulamalarını GitLab kullanarak CI/CD (Continuous Integration/Continuous Deployment) ile otomatik olarak dağıtmak isteyen yazılımcılar için hazırlanmıştır. GitLab Runner ile, her bir push (gönderim) işleminin otomatik olarak bağımlılıkları yüklemesi, kod kalitesini kontrol etmesi, testleri çalıştırması, Docker imajını oluşturması ve sunucuya dağıtması sağlanacaktır.
Ne Yapacağız
5 aşamadan oluşan bir pipeline oluşturacağız:
prepare → quality → test → build → deploy| Aşama | Açıklama |
|---|---|
prepare | Composer ile bağımlılıkları yükler |
quality | Laravel Pint ile kod stilini kontrol eder |
test | Pest ile testleri çalıştırır |
build | Docker imajını oluşturur ve Registry’ye yükler |
deploy | VPS’ye SSH ile dağıtım yapar |
Ön Gereksinimler
- GitLab hesabı (gitlab.com ya da kendi sunucunuzda barındırma)
- Ubuntu/Debian yüklü VPS ve Docker kurulumu
- Pest ile yapılandırılmış bir Laravel projesi
- VPS’ye SSH erişimi
Bölüm 1: GitLab Runner
GitLab kendiliğinden bir komut çalıştırmaz. Bunu yapan GitLab Runner‘dır; bu, herhangi bir makineye (VPS’nizde, bir sunucuya veya bilgisayarınıza) kurarak GitLab’dan çalıştırılacak işlerin beklenmesini sağlar.
Runner Nasıl Çalışır
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ GitLab │◄─polling─│ GitLab Runner │──executa─►│ Kodununuz │
│ (sunucu) │ │ (VPS'nizde) │ │ │
└──────────────┘ └──────────────────┘ └──────────────┘
Runner, GitLab’ın her birkaç saniyede bir iş olup olmadığını kontrol etmesini sağlar. Bir iş mevcut olduğunda, alır, çalıştırır ve sonucu geri döner. Dilediğiniz kadar runner’ı farklı makinelerde kullanabilirsiniz ve GitLab bu işleri aralarında dağıtır.
Runner Çalıştırıcı Türleri
| Çalıştırıcı | Nasıl çalışır | Ne Zaman Kullanılır |
|---|---|---|
| Shell | Komutları doğrudan sunucuda çalıştırır | Basit, izolasyon yok |
| Docker | Her bir işin içinde bir konteynerde çalışır | Tavsiye Edilir — tamamen izolasyon |
Biz Docker çalıştırıcısını kullanacağız; bu sayede her iş bir konteyner içinde tanımladığınız imaj ile çalışacaktır. Bu, ortamın her zaman temiz ve tekrar edilebilir olmasını sağlar.
GitLab Runner’ı VPS üzerine Kurma
VPS’nize SSH ile bağlanın ve şu komutları çalıştırın:
# GitLab Runner'ı resmi deposunu ekle
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
# Runner'ı kur
sudo apt-get install -y gitlab-runner
# Servisin çalıştığını kontrol et
sudo systemctl status gitlab-runner
💡 Runner, bir sistem servisi olarak (
systemd) çalışır ve sunucu ile birlikte otomatik olarak başlatılır.
Runner’ı GitLab Üzerinde Kaydetme
Runner kurulduktan sonra, bunu GitLab projenizde kaydetmeniz gerekiyor. Bu aşama, ajansı repozitora bağlar.
GitLab’da: Settings > CI/CD > Runners > New project runner yolunu izleyin.
GitLab, bir kayıt tokeni oluşturacaktır. VPS’ye geri dönün ve şu komutu çalıştırın:
sudo gitlab-runner registerKomut birkaç etkileşimli soruyu yöneltecek:
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
Kayıttan sonraki Runner, GitLab arayüzünde “Çevrimiçi” olarak görünecektir.
Docker-in-Docker için Ek Yapılandırma
Docker build işlerinin konteynerler içinde çalışabilmesi için ek bir yapılandırma gereklidir. /etc/gitlab-runner/config.toml dosyasını düzenleyin:
[[runners]]
name = "vps-production-runner"
url = "https://gitlab.com"
token = "glrt-xxxxxxxxxxxxxxxxxxxx"
executor = "docker"
[runners.docker]
image = "php:8.4-cli-bookworm"
privileged = true # Docker-in-Docker için gerekli
volumes = [
"/certs/client", # TLS sertifikaları için
"/cache"
]
privileged = true seçeneği, iş konteynerinin host üzerindeki Docker daemon’una erişebilmesi için gereklidir.
Düzenleme yaptıktan sonra Runner’ı yeniden başlatın:
sudo systemctl restart gitlab-runner⚠️ Güvenlik:
privileged = trueseçeneğini yalnızca Docker imajları oluşturan runnlerlar için kullanın. Test runnerları içinprivileged = falsetutun.
Bölüm 2: .gitlab-ci.yml Dosyası
Pipeline mantığı, projenizin köküne yerleştirilecek tek bir dosya olan .gitlab-ci.yml‘de bulunur. GitLab, her gönderiminizde bu dosyayı okuyarak uygun pipeline’ı oluşturur.
Bu bölümde, dosyayı bölüm bölünerek oluşturacağız.
Global Yapılar
# .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
Açıklama:
interruptible: true— yeni bir commit geldiğinde, mevcut çalışan pipeline iptal edilir. Bu, runner kaynaklarını tasarruf etmeyi ve sıralı yapıların atlanmasını önler.retry— pipeline, altyapı hatalarını (runner’ın çökmesi, zaman aşımı gibi) göz ardı ederek, sadece kod hatalarına dayanarak yeniden dener.workflow.rules— hangi durumların pipeline oluşturacağını belirler. Listelenmeyen dallardan (örneğinchore/fix-typo) hiçbir pipeline oluşturulmaz. Sonundakiwhen: neverseçeneği, açıkça tanımlanmayan tüm durumları “yoksayarak” bir “else” niteliği taşır.
Aşamalar ve Değişkenler
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şama sırası, yürütme dizisini belirler. Bir aşama yalnızca tüm önceki aşamadaki işler başarıyla geçince başlar. Değişkenler, tüm işler için tutarlı bir ortam sağlamaktadır. COMPOSER_CACHE_DIR ayarı, Composer’ın önbelleğini proje içine yönlendirir. Bu, sonraki pipeline’lar arasında önbellek ayarlamayı kolaylaştırır.
Yeniden Kullanılabilir Şablonlar
# Tüm PHP işler için temel şablon
.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
# İşlerdeki tekrarları önlemek için kural ankrajları
.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
GitLab CI’nin iki güçlü özelliği:
- İş şablonları (ön ek
.): gerçek iş yaratmaz, diğer işler için sadece temel olarak kullanılabilir. - YAML ankrajları (
&tanımlar,*referans verir): aynı kuralları her işte yinelemekten kaçınır. Bir dal eklemeniz gerektiğinde, sadece bir yerde düzenlersiniz.
Bölüm 3: Pipeline İşleri
Aşama prepare: Bağımlılıkların Yüklenmesi
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
Bu iş, tüm işlerin temelidir. Bağımlılıkları yükler ve sonraki tüm işlere sunar.
artifacts mekanizması:
vendor/dizinleri ve.envdosyası, işin sonunda “paketlenir” ve GitLab’da geçici olarak saklanır.- Herhangi bir iş,
needs: [{job: composer:install, artifacts: true}]tanımladığında, bu dosyaları otomatik olarak alır; yeniden indirme gerektirmez. expire_in: 2 hoursotomatik temizleme garantisi sağlar.
cache, artifacts’tan farklıdır: farklı pipeline’lar arasında kalıcıdır (sadece aynı pipeline’ın işleri arasında değil). Bu, sonraki build’lerin hızını artırır çünkü Composer, her seferinde internetteki paketleri indirmek zorunda kalmaz, yalnızca composer.lock‘da bir değişiklik olup olmadığını kontrol eder.
Aşama quality: Pint ile Stil Kontrolü
pint:
extends: .php-env
stage: quality
needs:
- job: composer:install
artifacts: true
script:
- vendor/bin/pint --test
rules:
*rules-ci
Laravel Pint, Laravel’in resmi kod formatlayıcısıdır; PHP-CS-Fixer’a dayanmaktadır. --test bayrağı, dosyaların değiştirilmediğini kontrol eder, yalnızca pint.json dosyasındaki (veya varsayılan olarak laravel presetini kullanan) kurallara uygun olup olmadığını kontrol eder.
Bazı dosyalar kötü formatlandığında, Pint sıfır olmayan bir çıkış kodu döndürür, bu iş başarısız olur ve pipeline durur, testler çalıştırılmadan önce. Bu, geliştiriciye hızlı bir geri bildirim sağlar.
💡 Düzenleyicinizi dosyayı kaydettiğinizde Pint’i çalışacak şekilde ayarlayın. CI bir güvenlik ağıdır, ana iş akışınız olmamalıdır.
Aşama test: Otomatik Testler ile Pest
pest:
extends: .php-env
stage: test
needs:
- job: composer:install
artifacts: true
script:
- php artisan test --compact
rules:
*rules-ci
Pest, PHP için şık bir test çerçevesidir. php artisan test ise Laravel için rahat bir uygulayıcıdır.
--compact bayrağı, sonuçları sıkıştırılmış bir biçimde gösterir; bu, CI logları için idealdir, burada nelerin geçtiğini ve nelerin başarısız olduğunu hızlıca tespit etmek istersiniz.
needs hakkında: işlerden bağımsız bağımlılıkları tanımlamak için kullanılır, dolayısıyla pint ve pest paralel olarak çalışabilir (her ikisi de yalnızca composer:install‘a bağlıdır). test aşaması sadece quality aşaması tamamlandığında başlar, fakat aynı aşama içinde, bağımı olmayan işler aynı anda çalışır.
Aşama build: Docker İmajı Oluşturma ve Yayınlama
.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 "✓ Yayınlanan: ${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 "✓ Yayınlanan sürüm ${RELEASE_VERSION}"
rules:
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
GitLab’in otomatik değişkenlarının analizi:
| Değişken | İçerik |
|---|---|
$CI_REGISTRY | GitLab Container Registry URL’si |
$CI_REGISTRY_USER | Kimlik doğrulama için kullanıcı (otomatik) |
$CI_REGISTRY_PASSWORD | Kimlik doğrulama için şifre (otomatik) |
$CI_REGISTRY_IMAGE | İmajın tam yolu (ör. registry.gitlab.com/grup/proje) |
$CI_COMMIT_SHORT_SHA | Commit hash’inin ilk 8 karakteri |
$CI_COMMIT_TAG | Pipelines’i tetikleyen Git etiketi (ör. v1.4.2) |
Commit SHA’sı ile etiketlemenin önemi:
registry.gitlab.com/seu-grupo/api:latest ← mevcut üretim versiyonu
registry.gitlab.com/seu-grupo/api:1.4.2 ← semantik versiyon
registry.gitlab.com/seu-grupo/api:a1b2c3d4 ← kesin commit
SHA etiketi ile hangi commit’in hangi imajı ürettiğini her zaman bilirsiniz. Bu, izlenebilirlik ve geri dönebilme içindir; yalnızca önceki commit’in SHA’sını bilmeniz yeterlidir ve sunucudaki etiketi değiştirebilirsiniz.
docker:27.4.0-dind (Docker-in-Docker) servisi, CI konteyneri içinde Docker komutları çalıştırmanızı sağlar. Bu nedenle, Runner’ın çalıştırıcısının privileged = true olması gerekir.
Aşama deploy: VPS’ye SSH ile Dağıtım
Bu, nihai aşamadır ve sihirin gerçekleştiği yerdir. İş, VPS’nize SSH ile bağlanır ve çalışan konteyneri günceller.
.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"
Deploy scriptini analiz etme:
docker pull— Sunucuya en son imajı indirir.docker stop+docker rm— mevcut konteyneri durdurur ve siler.|| truekullanımı, konteyner mevcut olmadığında script’in başarısız olmasını engeller (ilk çalıştırma).docker run— yeni konteyneri üretim yapılandırmalarıyla başlatır.--env-fileseçeneği, sunucudaki bir.envdosyasına işaret eder; asla üretim kimlik bilgilerini repozitora taşımayın.docker image prune -f— kullanılmayan eski imajları temizler, böylece VPS disk alanı dolmaz.
when: manual üretim dağıtımı için güvenlik nedeniyle kasıtlı bir karardır: üretim dağıtımı otomatik olarak gerçekleşmez. İş, GitLab arayüzünde onay bekler, bir geliştirici veya teknoloji lideri “Oynat” butonuna tıklamak zorundadır.
Bölüm 4: SSH Anahtarlarının Yapılandırılması
Deploy’un çalışması için, Runner’ın VPS’ye parola olmadan SSH ile kimlik doğrulaması yapması gerekir. Bunu GitLab’ın değişkenlerini kullanarak güvenli bir şekilde ayarlayacağız.
Anahtar Çifti Oluşturma
Yerel makinenizde (veya güvenli bir yerde), CI için ayrılmış bir anahtar çifti oluşturun:
ssh-keygen -t ed25519 -C "gitlab-ci-deploy" -f ~/.ssh/gitlab_ci_deploy -N ""Bu, iki dosya oluşturur:
~/.ssh/gitlab_ci_deploy— özel anahtar (GitLab’a gidecek)~/.ssh/gitlab_ci_deploy.pub— genel anahtar (sunucuya gidecek)
Genel Anahtarı Sunucuya Ekleme
# Genel anahtarın içeriğini kopyalayın
cat ~/.ssh/gitlab_ci_deploy.pub
# Sunucuya, dağıtım kullanıcısının authorized_keys dosyasına ekleyin
echo "GENEL_ANAHTAR_İÇERİĞİ" >> ~/.ssh/authorized_keys
Known Hosts’a Ulaşma
# Yerel makinenizde, sunucunun parmak izini alın
ssh-keyscan -H SUNUCU_IP_DA_VPS
Çıktıyı kopyalayın (şöyle görünmelidir: |1|abc123...|ssh-ed25519 AAAA...).
GitLab’daki Değişkenleri Yapılandırma
Settings > CI/CD > Variables gidin ve aşağıdakileri ekleyin:
| Değişken | Değer | Tür | Korunmuş |
|---|---|---|---|
SSH_PRIVATE_KEY | gitlab_ci_deploy içeriği | File | ✅ Evet |
SSH_KNOWN_HOSTS | ssh-keyscan çıktısı | Variable | ✅ Evet |
SSH_USER | VPS’deki SSH kullanıcısı (örneğin: ubuntu) | Variable | ✅ Evet |
SSH_HOST_PRODUCTION | VPS’nin IP veya host adı | Variable | ✅ Evet |
SSH_HOST_DEVELOP | Geliştirme sunucusunun IP veya host adı | Variable | ✅ Evet |
DEPLOY_PRODUCTION_URL | Üretimdeki API URL’si | Variable | Hayır |
DEPLOY_DEVELOP_URL | Geliştirme ortamının URL’si | Variable | Hayır |
⚠️ Herhangi bir hassas değişkeni Masked ve Protected olarak işaretleyin. Masked, değerin günlüklerde görünmesini engeller. Protected, kullanımı korunmuş dallara ve etiketlere kısıtlar.
Bölüm 5: Multi-Stage Dockerfile
Dockerfile, üretim imajları için ince ve güvenli olma amacıyla multi-stage build standardını kullanır:
# syntax=docker/dockerfile:1
# ──────────────────────────────────────────────
# Aşama 1: Bağımlılıkları Composer ile yükler
# ──────────────────────────────────────────────
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
# ──────────────────────────────────────────────
# Aşama 2: Final çalışma imajı
# ──────────────────────────────────────────────
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
# Sadece önceden oluşturulmuş vendor dizinini kopyalar
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"]
Neden iki aşama?
vendoraşaması, Composer’ın resmi imajını kullanır (git, unzip ve PHP paketlerini yüklemek için gerekli her şeyi içerir) ve optimize edilmişvendor/dizinini oluşturur.runtimeaşaması, temiz bir PHP imajı ile başlar ve yalnızca önceden oluşturulmuş olanvendor/dizinini kopyalar.- Son imaj, Composer, git veya herhangi bir build aracını içermez. Bu, imajın boyutunu küçültür ve üretim için saldırı yüzeyini azaltır.
Supervisor, aynı konteynerde iki süreci yönetir: PHP-FPM (PHP isteklerini işler) ve Nginx (HTTP isteklerini alır ve PHP-FPM’e yönlendirir). Stateless bir API için, bu basit ve etkili bir yaklaşımdır.
Bölüm 6: VPS’yi Dağıtıma Hazırlama
Direktör Yapısı
Sunucuda dizin yapısını oluşturun:
sudo mkdir -p /opt/apps/api
sudo chown -R $USER:$USER /opt/apps/apiÜretim için .env Dosyası
Sunucuda doğrudan .env dosyasını oluşturun:
nano /opt/apps/api/.envAPP_NAME="API Adı"
APP_ENV=production
APP_KEY=base64:SİFRE
APP_DEBUG=false
APP_URL=https://api.domeniniz.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=api_üretim
DB_USERNAME=kullanici
DB_PASSWORD=super_gizli_sifre
# ... diğer değişkenler
🔒 Bu dosya asla repozitora gitmemelidir. Üretim sırlarını içerir ve yalnızca sunucuda mevcut olmalıdır.
Registry’de Docker’ı Kimlik Doğrulama
Üretim sunucunuzda, gizli bir imajı docker pull yapabilmesi için GitLab Registry’ne Docker’ı kimlik doğrulaması yapın:
docker login registry.gitlab.comYa da, etkileşimli şifre olmadan otomatik yapmak için, GitLab’da bir Deploy Token oluşturun (Settings > Repository > Deploy tokens) ile read_registry iznine sahip olun ve şu komutu kullanın:
docker login registry.gitlab.com -u KULLANICI_ADI -p PAROLADocker kimlik bilgileri ~/.docker/config.json altında kaydedilir, bu nedenle gelecekteki docker pull çağrıları, manuel kimlik doğrulaması olmaksızın çalışacaktır.
Tam .gitlab-ci.yml Dosyası
# .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"
# ── Şablonlar ────────────────────────────────────────────────────────────────
.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
.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
# ── Aşama: 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
# ── Aşama: quality ────────────────────────────────────────────────────────────
pint:
extends: .php-env
stage: quality
needs:
- job: composer:install
artifacts: true
script:
- vendor/bin/pint --test
rules:
*rules-ci
# ── Aşama: test ───────────────────────────────────────────────────────────────
pest:
extends: .php-env
stage: test
needs:
- job: composer:install
artifacts: true
script:
- php artisan test --compact
rules:
*rules-ci
# ── Aşama: 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 "✓ Yayınlanan: ${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 "✓ Yayınlanan sürüm ${RELEASE_VERSION}"
rules:
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/
# ── Aşama: 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 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"
Tam Akış Diyagramı
Push na branch main
│
▼
┌────────────────────┐
│ composer:install │ ← Bağımlılıkları yükler, vendor/ ve .env oluşturur
└──────────┬─────────┘
│ artifacts
┌────┴────┐
▼ ▼
┌───────┐ ┌──────┐
│ pint │ │ pest │ ← Paralel olarak çalışır (her ikisi de composer:install'a bağlı)
└───────┘ └──┬───┘
│ geçti
▼
┌────────────────────┐
│ docker:build:main │ ← build + push :latest ve :sha
└──────────┬─────────┘
│
▼
┌───────────────────┐
│ deploy:production│ ← Manual onay bekler [▶ Play]
│ SSH → VPS │
│ docker pull │
│ docker run │
└───────────────────┘
Kontrol Listesi
İlk kez pipeline’ı test etmeden önce, şunları kontrol edin:
- [ ] GitLab Runner kurulu ve projeye kaydedildi
- [ ] Çalıştırıcı,
dockerolarak yapılandırıldı veprivileged = true - [ ] SSH anahtar çifti oluşturuldu ve yapılandırıldı
- [ ] Genel anahtar, VPS’deki
authorized_keysdosyasına eklendi - [ ] GitLab’da CI/CD değişkenleri yapılandırıldı
- [ ] Üretim için
.envdosyası sunucuda manuel olarak oluşturuldu - [ ] VPS’de Docker, GitLab Registry’de kimlik doğrulaması yapıldı
- [ ]
/opt/apps/apidizini VPS’de oluşturuldu - [ ] Projede
pint.jsondoğru bir şekilde ayarlandı (veya varsayılanlaravelpresetini kullanarak) - [ ] İlk gönderimden önce, Pest testleri yerel olarak geçiyor
Sonuç
Bu yapılandırma ile, repozitronuza her gönderim, kodun doğru formatlandığını, testlerin geçerli olduğunu ve istediğiniz zaman yeni sürümün sunucuya yalnızca bir tıklama ile teslim edilmesini sağlayan bir pipeline’ı tetikler.
Sonuç olarak, dağıtım konusunda kendinize güvenebilirsiniz. Artık manuel SSH yok, “bir şey bozulacak mı?” korkusu yok, artık Cuma günleri saat 17:00’de yüreğinizi ağızınıza getiren dağıtımlar yok.
Bu yapı, tek bir kişilik bir takım için yeterince basit ve projeyle birlikte büyüyecek kadar ölçeklenebilir; yeni ortamlar ekleyebilir, yeni statik analiz aşamaları (PHPStan gibi) ekleyebilir veya daha karmaşık bir orkestratöre geçebilirsiniz — tüm bunları yeniden yazmanıza gerek kalmadan.
Kaynak: Orijinal Makale
- Ne Yapacağız
- Ön Gereksinimler
- Bölüm 1: GitLab Runner
- Runner Nasıl Çalışır
- Runner Çalıştırıcı Türleri
- GitLab Runner’ı VPS üzerine Kurma
- Runner’ı GitLab Üzerinde Kaydetme
- Docker-in-Docker için Ek Yapılandırma
- Bölüm 2: .gitlab-ci.yml Dosyası
- Bölüm 3: Pipeline İşleri
- Aşama prepare: Bağımlılıkların Yüklenmesi
- Aşama quality: Pint ile Stil Kontrolü
- Aşama test: Otomatik Testler ile Pest
- Aşama build: Docker İmajı Oluşturma ve Yayınlama
- Aşama deploy: VPS’ye SSH ile Dağıtım
- Bölüm 4: SSH Anahtarlarının Yapılandırılması
- Anahtar Çifti Oluşturma
- Genel Anahtarı Sunucuya Ekleme
- Known Hosts’a Ulaşma
- GitLab’daki Değişkenleri Yapılandırma
- Bölüm 5: Multi-Stage Dockerfile
- Bölüm 6: VPS’yi Dağıtıma Hazırlama
- Tam .gitlab-ci.yml Dosyası
- Tam Akış Diyagramı
- Kontrol Listesi
- Sonuç


