Ten kompleksowy przewodnik przekształci Twój proces wdrażania Laravel w płynny, zautomatyzowany pipeline wykorzystujący Docker i GitHub Actions. Dowiedz się, jak skonfigurować niezawodne środowisko produkcyjne, które będzie skalować się wraz z Twoimi potrzebami.
📋 Spis treści
- Utworzenie użytkownika
- Początkowa konfiguracja serwera
- Konfiguracja klucza SSH dla GitHub Actions
- Dodanie sekretów GitHub
- Konfiguracja aplikacji
- Rozwiązywanie problemów
- Podsumowanie
0. Utworzenie użytkownika (opcjonalnie—możesz użyć swojego użytkownika nie-root)
# Utwórz użytkownika z odpowiednią grupą główną
sudo adduser deployer --ingroup www-data
sudo usermod -aG sudo deployer
1. Początkowa konfiguracja serwera
# Instalacja Dockera i Docker Compose
curl -fsSL https://get.docker.com | sudo sh
# Dodaj użytkownika do grupy Docker
sudo usermod -aG docker deployer
2. Konfiguracja klucza SSH dla GitHub Actions (jako użytkownik deployer)
# Utwórz katalog SSH
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Wygeneruj klucz SSH
ssh-keygen -t rsa -b 4096 -C "github-actions-deploy"
# Dodaj klucz publiczny do authorized_keys
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# Wyświetl klucz prywatny
cat ~/.ssh/id_rsa
3. Dodanie sekretów GitHub
Dodaj następujące sekrety do swojego repozytorium GitHub:
SSH_HOST
: Adres IP lub domena Twojego VPSSSH_USER
: deployerSSH_KEY
: Wygenerowany powyżej prywatny klucz SSHSSH_PORT
: 22 (lub Twój niestandardowy port SSH)
Dodaj zmienną dla pliku .env produkcyjnego:
ENV_FILE
: Zawartość Twojego pliku .env
4. Konfiguracja aplikacji
Utwórz następujące pliki w swoim projekcie Laravel:
4.1 Pliki konfiguracyjne Docker
- Utwórz
docker/php/Dockerfile
:
FROM dommin/php-8.4-fpm-alpine:latest
USER root
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
RUN sed -i 's/user = www-data/user = appuser/g' /usr/local/etc/php-fpm.d/www.conf && \
sed -i 's/group = www-data/group = appuser/g' /usr/local/etc/php-fpm.d/www.conf
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist --no-scripts
COPY --chown=appuser:appuser . .
COPY --chown=appuser:appuser --from=node /var/www/public/build /var/www/public/build
COPY --chown=appuser:appuser docker/supervisord.conf /etc/supervisord.conf
COPY --chown=appuser:appuser docker/php/php.ini /usr/local/etc/php/php.ini
COPY --chown=appuser:appuser docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf
RUN mkdir -p \
storage/framework/{cache,sessions,views} \
storage/app/public \
public/storage \
bootstrap/cache \
storage/logs \
/var/log/supervisor \
/var/run/php \
/var/log/php-fpm \
/home/appuser/.cache/puppeteer \
&& chown -R appuser:appuser \
storage \
bootstrap/cache \
public \
/var/log/supervisor \
/var/run/php \
/var/log/php-fpm \
/home/appuser/.cache/puppeteer \
&& chmod -R 775 storage bootstrap/cache
RUN ln -s /var/www/storage/app/public /var/www/public/storage
USER appuser
EXPOSE 9000
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
1.1 Utwórz docker/supervisord.conf
:
[supervisord]
nodaemon=true
logfile=/var/www/storage/logs/supervisord.log
pidfile=/var/run/supervisord.pid
user=appuser
[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
user=appuser
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
[program:horizon]
command=php /var/www/artisan horizon
user=appuser
autostart=true
autorestart=true
stdout_logfile=/var/www/storage/logs/horizon.log
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
stopwaitsecs=3600
[program:scheduler]
command=php /var/www/artisan schedule:work
user=appuser
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
[program:reverb]
command=php /var/www/artisan reverb:start --debug
user=appuser
autostart=true
autorestart=true
stdout_logfile=/var/www/storage/logs/reverb.log
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
stopwaitsecs=3600
1.3 Utwórz docker/php/php.ini
:
[PHP]
expose_php = Off
max_execution_time = 120
max_input_time = 120
memory_limit = 256M
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.enable_cli=0
[File Uploads]
upload_max_filesize = 64M
post_max_size = 64M
max_file_uploads = 20
[Date]
date.timezone = Europe/Warsaw
[Error]
display_errors = On
display_startup_errors = On
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
error_log = /var/www/storage/logs/php-error.log
1.4 Utwórz docker/php/www.conf
:
[www]
user = www-data
group = www-data
listen = 0.0.0.0:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.status_path = /status
access.log = /proc/self/fd/1
catch_workers_output = yes
decorate_workers_output = no
- Utwórz
docker/nginx/Dockerfile
:
FROM nginx:stable-alpine
# Declare build arguments
ARG HTPASSWD_USER
ARG HTPASSWD_PASS
# Create necessary directories first
RUN mkdir -p /var/www/public
COPY --from=node /var/www/public/build /var/www/public/build
COPY public/*.php public/*.txt public/*.ico /var/www/public/
COPY docker/nginx/conf.d /etc/nginx/conf.d
# Create .htpasswd file for basic auth (optional)
RUN if [ -n "$HTPASSWD_USER" ] && [ -n "$HTPASSWD_PASS" ]; then \
apk add --no-cache apache2-utils && \
htpasswd -b -c /etc/nginx/.htpasswd "$HTPASSWD_USER" "$HTPASSWD_PASS"; \
fi
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
- Utwórz
docker/node/Dockerfile
:
FROM node:22-alpine
WORKDIR /var/www
COPY package*.json ./
RUN npm ci
COPY vite.config.ts tsconfig.json ./
COPY docker/node/.env .env
COPY . .
RUN npm run build
- Utwórz
docker-compose.production.yml
:
services:
app:
image: ${REGISTRY}/${PHP_IMAGE_NAME}:${TAG:-latest}
container_name: laravel_app
restart: unless-stopped
working_dir: /var/www
env_file: .env
volumes:
- laravel_storage:/var/www/storage
- ./.env:/var/www/.env
ports:
- '9000:9000'
- '8080:8080'
networks:
- laravel_network
depends_on:
- redis
nginx:
image: ${REGISTRY}/${NGINX_IMAGE_NAME}:${TAG:-latest}
container_name: laravel_nginx
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- laravel_storage:/var/www/storage
networks:
- laravel_network
depends_on:
- app
redis:
image: redis:alpine
container_name: laravel_redis
restart: unless-stopped
networks:
- laravel_network
volumes:
- redis_data:/data
networks:
laravel_network:
driver: bridge
volumes:
laravel_storage:
redis_data:
4.2 Workflow GitHub Actions
Utwórz .github/workflows/workflow.yml
:
name: 🚀 Build, Push and Deploy
on:
push:
branches:
- main
env:
REGISTRY: ghcr.io
NODE_IMAGE_NAME: dommmin/laravel-production-node-builder
PHP_IMAGE_NAME: dommmin/laravel-production-php
NGINX_IMAGE_NAME: dommmin/laravel-production-nginx
DOCKER_BUILDKIT: 1
jobs:
build:
name: 🏗️ Build and Push Images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: ${{ runner.os }}-buildx-
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create .env files
run: |
mkdir -p docker/node docker/php
echo "${{ vars.ENV_FILE }}" > docker/node/.env
echo "${{ vars.ENV_FILE }}" > docker/php/.env
cat docker/node/.env
- name: Build and push Node builder image
uses: docker/build-push-action@v5
with:
context: .
file: docker/node/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.NODE_IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
- name: Build and push PHP image
uses: docker/build-push-action@v5
with:
context: .
file: docker/php/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.PHP_IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
build-contexts: |
node=docker-image://${{ env.REGISTRY }}/${{ env.NODE_IMAGE_NAME }}:latest
- name: Build and push Nginx image
uses: docker/build-push-action@v5
with:
context: .
file: docker/nginx/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.NGINX_IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
build-contexts: |
node=docker-image://${{ env.REGISTRY }}/${{ env.NODE_IMAGE_NAME }}:latest
build-args: |
HTPASSWD_USER=${{ secrets.HTPASSWD_USER }}
HTPASSWD_PASS=${{ secrets.HTPASSWD_PASS }}
deploy:
name: 🚀 Deploy to Production
needs: build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4 # Critical for accessing files!
- name: Setup SSH Authentication
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: ${{ secrets.SSH_KEY }}
- name: Configure known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Prepare environment file
run: |
echo "${{ vars.ENV_FILE }}" > .env
{
echo "REGISTRY=${{ env.REGISTRY }}"
echo "PHP_IMAGE_NAME=${{ env.PHP_IMAGE_NAME }}"
echo "NGINX_IMAGE_NAME=${{ env.NGINX_IMAGE_NAME }}"
echo "TAG=latest"
} >> .env
- name: Transfer deployment files
run: |
scp -P ${{ secrets.SSH_PORT }} \
docker-compose.production.yml \
${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:~/laravel/docker-compose.yml
scp -P ${{ secrets.SSH_PORT }} \
.env deploy.sh \
${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:~/laravel/
- name: Trigger deployment script
run: |
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \
"cd ~/laravel && chmod +x deploy.sh && ./deploy.sh"
4.3 Skrypt wdrożeniowy
Utwórz deploy.sh
:
#!/bin/bash
# Fail immediately if any command fails
set -eo pipefail
# Deployment header
echo "🚀 Starting production deployment..."
echo "🕒 $(date)"
# Pull latest images
echo "📥 Pulling updated Docker images..."
docker compose pull
# Stop existing containers if running
echo "🛑 Stopping existing containers..."
docker compose down --remove-orphans
# Start fresh containers
echo "🔄 Starting new containers..."
docker compose up -d --force-recreate
# Run application maintenance
echo "🔧 Running application maintenance tasks..."
docker compose exec -T app php artisan optimize:clear
docker compose exec -T app php artisan optimize
docker compose exec -T app php artisan storage:link
docker compose exec -T app php artisan migrate --force
# Cleanup old Docker objects
echo "🧹 Cleaning up unused Docker resources..."
docker system prune --volumes -f
# Success message
echo "✅ Deployment completed successfully!"
echo "🕒 $(date)"
4.4 Struktura katalogów
Twój projekt Laravel powinien mieć następującą strukturę:
.
├── .github
│ └── workflows
│ └── workflow.yml
├── docker
│ ├── node
│ │ ├── Dockerfile
│ ├── nginx
│ │ ├── Dockerfile
│ │ └── conf.d
│ │ └── default.conf
│ └── php
│ ├── Dockerfile
│ ├── php.ini
│ ├── supervisord.conf
│ └── www.conf
├── docker-compose.production.yml
├── deploy.sh
└── ... (pozostałe pliki Laravel)
Rozwiązywanie problemów
-
Problemy z uprawnieniami:
# Sprawdź dostęp do Dockera docker ps
-
Problemy z Dockerem:
# Sprawdź status Dockera sudo systemctl status docker
-
Błędy wdrożenia: Sprawdź logi GitHub Actions, aby znaleźć szczegółowe komunikaty o błędach.
Podsumowanie
Po wykonaniu tych kroków, Twój serwer będzie gotowy do wdrożenia Laravel z wykorzystaniem Dockera. Konfiguracja zapewnia:
- Odpowiednie uprawnienia dla Dockera i Laravel
- Bezpieczny dostęp SSH dla GitHub Actions
- Trwałe przechowywanie danych Laravel
- Implementację najlepszych praktyk bezpieczeństwa