Blog

🚀 Docker na Produkcji: Automatyczne Wdrażanie z GitHub Actions

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

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 VPS
  • SSH_USER: deployer
  • SSH_KEY: Wygenerowany powyżej prywatny klucz SSH
  • SSH_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

  1. 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
  1. 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;"]
  1. 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
  1. 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/[email protected]
        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

Wsparcie istniejącego systemu

Potrzebujesz pomocy z działającą aplikacją?

Pomagam firmom rozwijać działające systemy, porządkować wdrożenia i dodawać nowe funkcje bez dokładania chaosu do projektu.

Komentarze (0)
Zaloguj się, aby dodać komentarz

Musisz być zalogowany, aby dodać komentarz.

Zaloguj się

Potrzebujesz kogoś, kto weźmie odpowiedzialność za kolejny krok?

Porozmawiajmy o Twoim projekcie i określmy zakres, który ma sens dla Twoich celów.