본문 바로가기

리눅스

라라벨 개발 환경을 도커 컨테이너로 구성하는 방법

반응형

라라벨 개발 환경을 도커 컨테이너로 구성하는 방법

테스트 환경

nginx 버전 정보

docker compose exec nginx nginx -v

php 버전 정보

docker compose exec php-fpm php --version

composer 버전 정보

docker compose exec php-fpm composer --version

라라벨 artisan 버전 정보

docker compose exec php-fpm php artisan --version

작업 디렉토리 이동

mkdir -p /apps/container/docker-laravel-development-environment
cd /apps/container/docker-laravel-development-environment

라라벨 개발 환경에 필요한 파일 생성

Docker Compose 파일

더보기

---

cat > docker-compose.yml << 'EOF'
# docker-compose.yml
services:

  nginx:
    image: nginx:1.29-alpine
    container_name: nginx
    hostname: nginx
    volumes:
      - ./html:/var/www/html
      - ./docker/nginx/conf.d:/etc/nginx/conf.d
    depends_on:
      - php-fpm
    ports:
      - "80:80"
    networks:
      - laravel-network

  php-fpm:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: php-fpm
    hostname: php-fpm
    environment:
      - DB_HOST=mysql
      - DB_DATABASE=laravel
      - DB_USERNAME=laravel
      - DB_PASSWORD=secret
    volumes:
      - ./html:/var/www/html
      - ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
    networks:
      - laravel-network
    healthcheck:
      test: ["CMD", "php", "-r", "echo 'OK';"]
      interval: 30s
      timeout: 10s
      retries: 3

  mysql:
    image: mysql:8.4
    container_name: mysql
    hostname: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
      MYSQL_USER: laravel
      MYSQL_PASSWORD: secret
    ports:
      - "3306:3306"
    networks:
      - laravel-network

networks:
  laravel-network:
    driver: bridge
EOF

---

PHP-FPM Dockerfile

더보기

---

cat > docker/php/Dockerfile << 'EOF'
# docker/php/Dockerfile
FROM php:8.3-fpm-alpine

# 1. 시스템 패키지 설치
RUN apk update && apk add --no-cache \
    # 기본 도구
    curl git unzip vim \
    # PHP 확장 의존성
    libpng-dev libjpeg-turbo-dev libwebp-dev freetype-dev \
    libxml2-dev oniguruma-dev libzip-dev postgresql-dev \
    # Redis 빌드 도구(일시적)
    autoconf g++ make

# 2. PHP 확장 설치
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp && \
    docker-php-ext-install -j$(nproc) \
    pdo_mysql mbstring xml zip bcmath pcntl gd

# 3. Redis 확장 설치
RUN pecl install redis && docker-php-ext-enable redis

# 4. 빌드 도구 정리 (이미지 용량 최적화)
RUN apk del autoconf g++ make && \
    rm -rf /tmp/pear && \
    docker-php-source delete

# 5. Composer 설치
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# 6. 작업 디렉토리 및 권한 설정
WORKDIR /var/www/html

# 7.권한 설정
RUN chmod -R 775 /var/www/html

# PHP-FPM 사용자 생성 및 권한 설정
#RUN chown -R www-data:www-data /var/www/html
#USER www-data

# 8. health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
    CMD curl -f http://localhost/ping || exit 1
EOF

---

PHP 설정 파일

더보기

---

cat > docker/php/php.ini << 'EOF'
# docker/php/php.ini
memory_limit = 512M
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300
max_input_time = 300

; 에러 리포트
display_errors = On
error_reporting = E_ALL

; OPcache (성능 향상)
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
EOF

---

Nginx 설정

더보기

---

cat > docker/nginx/nginx.conf << 'EOF'
# docker/nginx/nginx.conf
user www-data www-data;

worker_processes auto;

error_log /var/log/nginx/error.log notice;

pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    
    server_tokens off;

    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;
    gzip on;

    include /etc/nginx/conf.d/*.conf;
}
EOF

---

Nginx 가상 호스트 설정

더보기

---

cat > docker/nginx/conf.d/default.conf << 'EOF'
# docker/nginx/conf.d/default.conf
server {
    listen 80;
    server_name localhost;
    root /var/www/html/public;
    index index.php index.html;

    error_log /var/log/nginx/default-error.log;
    access_log /var/log/nginx/default-access.log;

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

    location ~ \.php$ {
        fastcgi_pass php-fpm:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        
        fastcgi_read_timeout 300;
        fastcgi_send_timeout 300;
    }

    location ~ /\.ht {
        deny all;
    }
}
EOF

---

Docker 컨테이너 빌드 및 실행

docker compose down -v
docker compose build --no-cache php-fpm
docker compose up -d

Nginx 정보 확인

docker compose exec nginx nginx -v

PHP 정보 확인

docker compose exec php-fpm php -v
docker compose exec php-fpm sh -c "php -m | egrep 'redis|gd'"

Nginx 테스트

docker compose exec nginx mkdir -p /var/www/html/public
docker compose exec nginx sh -c "echo 'Container Nginx Server' > /var/www/html/public/index.html"

Nginx 기본 페이지 접속 테스트

curl -I http://localhost

라라벨 프로젝트 생성

public 디렉토리 삭제

docker compose exec nginx rm -rf /var/www/html/public

라라벨 프로젝트 생성

docker compose exec php-fpm composer create-project laravel/laravel . --prefer-dist --no-interaction

라라벨 파일 확인

docker compose exec php-fpm ls -la /var/www/html

환경 설정

docker compose exec php-fpm cp .env.example .env

.env 편집

vim html/.env
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

REDIS_CLIENT=phpredis
REDIS_CLUSTER=false
REDIS_HOST=192.168.0.110
REDIS_PORT=6379
REDIS_PASSWORD=redis_password
REDIS_DB=0

APP_KEY 생성

docker compose exec php-fpm php artisan key:generate

APP_KEY 확인

docker compose exec php-fpm grep APP_KEY .env

스토리지 링크 및 권한 설정

  • 저장소 권한 설정
docker compose exec php-fpm chmod -R 775 storage bootstrap/cache
  • 소유권 설정
docker compose exec php-fpm chown -R www-data:www-data storage
docker compose exec php-fpm chown -R www-data:www-data bootstrap/cache
docker compose exec php-fpm php artisan migrate

캐시 클리어

docker compose exec php-fpm php artisan config:clear
docker compose exec php-fpm php artisan cache:clear

페이지 접속 테스트

curl -s -o /dev/null -w "%{http_code}\n" -I http://localhost

Hello World 페이지 접속 테스트

라우트 파일 생성

docker compose exec php-fpm sh -c 'cat > routes/web.php << "EOF"
<?php
Route::get("/", function() { return "Laravel, Hello World!"; });
EOF'

파일 문법 검사

docker compose exec php-fpm php -l routes/web.php

라우트 목록 확인

docker compose exec php-fpm php artisan route:list

페이지 접속 테스트

curl http://localhost
Laravel, Hello World!

 

routes/web.php 생성

더보기

---

docker compose exec php-fpm sh -c 'cat > routes/web.php << "EOF"
<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

Route::get("/", function () {
    return response()->json([
        "message" => "Laravel with MySQL + Redis",
        "status" => "success",
        "timestamp" => now()->toDateTimeString()
    ]);
});

// MySQL + Redis 통합 테스트
Route::get("/test-integrated", function () {
    $results = [];
    
    try {
        // 1. MySQL 연결 테스트
        DB::connection()->getPdo();
        $results["mysql"] = "✅ MySQL 연결 성공";
        
        // 2. Redis 연결 테스트
        Redis::ping();
        $results["redis"] = "✅ Redis 연결 성공";
        
        // 3. MySQL에 사용자 데이터 저장
        Schema::dropIfExists("test_users");
        Schema::create("test_users", function ($table) {
            $table->id();
            $table->string("name");
            $table->string("email")->unique();
            $table->integer("login_count")->default(0);
            $table->timestamps();
        });
        
        $userId = DB::table("test_users")->insertGetId([
            "name" => "통합테스트사용자",
            "email" => "integrated@" . now()->format("His") . ".com",
            "login_count" => 0,
            "created_at" => now(),
            "updated_at" => now()
        ]);
        $results["mysql_write"] = "✅ MySQL 데이터 저장 (ID: " . $userId . ")";
        
        // 4. Redis에 사용자 캐시 저장
        $userKey = "user:" . $userId;
        Redis::hset($userKey, "name", "통합테스트사용자");
        Redis::hset($userKey, "email", "integrated@" . now()->format("His") . ".com");
        Redis::hset($userKey, "last_login", now()->toDateTimeString());
        Redis::expire($userKey, 300); // 5분 TTL
        
        $results["redis_write"] = "✅ Redis 사용자 캐시 저장";
        
        // 5. Redis에서 사용자 정보 읽기
        $cachedUser = Redis::hgetall($userKey);
        $results["redis_read"] = $cachedUser ? "✅ Redis 캐시 읽기 성공" : "❌ Redis 캐시 읽기 실패";
        $results["cached_user"] = $cachedUser;
        
        // 6. MySQL에서 데이터 읽기
        $dbUser = DB::table("test_users")->where("id", $userId)->first();
        $results["mysql_read"] = $dbUser ? "✅ MySQL 데이터 읽기 성공" : "❌ MySQL 데이터 읽기 실패";
        
        // 7. 로그인 카운트 업데이트 (MySQL + Redis 동기화)
        DB::table("test_users")->where("id", $userId)->increment("login_count");
        Redis::hset($userKey, "login_count", 
            DB::table("test_users")->where("id", $userId)->value("login_count")
        );
        $results["sync"] = "✅ MySQL-Redis 데이터 동기화";
        
        // 8. 캐시 시스템 테스트
        $cacheKey = "user_profile:" . $userId;
        $cachedProfile = Cache::remember($cacheKey, 60, function () use ($userId) {
            return DB::table("test_users")->where("id", $userId)->first();
        });
        $results["cache_system"] = "✅ 라라벨 캐시 시스템 작동";
        
        // 9. 세션 테스트 (Redis 사용)
        session(["user_id" => $userId, "last_activity" => now()]);
        $results["session"] = "✅ Redis 세션 저장";
        
        $results["status"] = "success";
        $results["timestamp"] = now()->toDateTimeString();
        
    } catch (Exception $e) {
        $results["status"] = "error";
        $results["message"] = $e->getMessage();
        $results["timestamp"] = now()->toDateTimeString();
    }
    
    // 정리
    try {
        Schema::dropIfExists("test_users");
        Redis::del("user:" . $userId);
        $results["cleanup"] = "✅ 테스트 데이터 정리 완료";
    } catch (Exception $e) {
        $results["cleanup"] = "⚠️ 정리 실패: " . $e->getMessage();
    }
    
    return response()->json($results);
});

// 캐시 성능 테스트
Route::get("/test-cache-performance", function () {
    $results = [];
    $iterations = 10;
    
    // MySQL 직접 조회 시간 측정
    $startTime = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        DB::select("SELECT SLEEP(0.01)"); // 작은 지연 시뮬레이션
    }
    $mysqlTime = microtime(true) - $startTime;
    
    // Redis 조회 시간 측정
    $startTime = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        Redis::get("test_key_" . $i);
    }
    $redisTime = microtime(true) - $startTime;
    
    // 캐시 시스템 시간 측정
    $startTime = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        Cache::get("test_key_" . $i);
    }
    $cacheTime = microtime(true) - $startTime;
    
    $results["mysql_time"] = round($mysqlTime, 4) . "초";
    $results["redis_time"] = round($redisTime, 4) . "초";
    $results["cache_time"] = round($cacheTime, 4) . "초";
    $results["iterations"] = $iterations;
    $results["performance_ratio"] = round($mysqlTime / $redisTime, 2) . "x faster";
    
    return response()->json($results);
});

// 실전 예제: 사용자 프로필 시스템
Route::get("/user-profile/{id}", function ($id) {
    $cacheKey = "user_profile_" . $id;
    
    // 캐시에서 사용자 프로필 가져오기 (없으면 MySQL에서 조회)
    $userProfile = Cache::remember($cacheKey, 300, function () use ($id) {
        $user = DB::table("users")->where("id", $id)->first();
        
        if ($user) {
            // 추가 데이터 처리 (가정)
            return [
                "id" => $user->id,
                "name" => $user->name,
                "email" => $user->email,
                "cached_at" => now()->toDateTimeString(),
                "source" => "database"
            ];
        }
        
        return null;
    });
    
    if (!$userProfile) {
        return response()->json([
            "status" => "error",
            "message" => "User not found"
        ], 404);
    }
    
    // 접근 통계를 Redis에 저장
    Redis::zincrby("user_access_stats", 1, $id);
    Redis::hset("user_last_access", $id, now()->toDateTimeString());
    
    return response()->json([
        "status" => "success",
        "data" => $userProfile,
        "access_stats" => [
            "rank" => Redis::zrevrank("user_access_stats", $id),
            "score" => Redis::zscore("user_access_stats", $id)
        ]
    ]);
});

// 대기열(Queue) 테스트
Route::get("/test-queue", function () {
    // 이메일 발송 작업을 대기열에 추가 (Redis 사용)
    $jobId = Illuminate\Support\Facades\Queue::push(new class implements Illuminate\Contracts\Queue\ShouldQueue {
        public function handle() {
            // 실제 작업 시뮬레이션
            Log::info("Queue job executed at: " . now()->toDateTimeString());
            Redis::incr("queue_job_count");
        }
    });
    
    return response()->json([
        "status" => "success",
        "message" => "Job added to queue",
        "job_id" => $jobId,
        "queue_driver" => config("queue.default"),
        "pending_jobs" => Redis::llen("queues:default")
    ]);
});

// 시스템 상태 모니터링
Route::get("/system-status", function () {
    $status = [];
    
    try {
        // MySQL 상태
        $status["mysql"] = [
            "connected" => true,
            "database" => DB::connection()->getDatabaseName(),
            "version" => DB::select("SELECT VERSION() as version")[0]->version,
            "tables" => count(DB::select("SHOW TABLES"))
        ];
    } catch (Exception $e) {
        $status["mysql"] = ["connected" => false, "error" => $e->getMessage()];
    }
    
    try {
        // Redis 상태
        $status["redis"] = [
            "connected" => true,
            "ping" => Redis::ping(),
            "info" => Redis::info("memory"),
            "keys_count" => Redis::dbsize()
        ];
    } catch (Exception $e) {
        $status["redis"] = ["connected" => false, "error" => $e->getMessage()];
    }
    
    // 캐시 상태
    $status["cache"] = [
        "driver" => config("cache.default"),
        "stores" => array_keys(config("cache.stores"))
    ];
    
    return response()->json($status);
});

// 사용자 로그인 시뮬레이션
Route::get("/login/{userId}", function ($userId) {
    // MySQL에서 사용자 확인 (가정)
    $user = DB::table("users")->where("id", $userId)->first();
    
    if (!$user) {
        return response()->json(["error" => "User not found"], 404);
    }
    
    // Redis에 세션 저장
    $sessionId = Str::random(40);
    Redis::setex("session:" . $sessionId, 3600, json_encode([
        "user_id" => $user->id,
        "name" => $user->name,
        "email" => $user->email,
        "login_time" => now()->toDateTimeString(),
        "ip" => request()->ip()
    ]));
    
    // 활성 세션 목록 업데이트
    Redis::sadd("active_sessions", $sessionId);
    
    return response()->json([
        "status" => "success",
        "message" => "Login successful",
        "session_id" => $sessionId,
        "user" => [
            "id" => $user->id,
            "name" => $user->name
        ]
    ]);
});

// 세션 확인
Route::get("/profile", function () {
    $sessionId = request()->header("X-Session-ID");
    
    if (!$sessionId) {
        return response()->json(["error" => "Session required"], 401);
    }
    
    $sessionData = Redis::get("session:" . $sessionId);
    
    if (!$sessionData) {
        return response()->json(["error" => "Invalid session"], 401);
    }
    
    $userData = json_decode($sessionData, true);
    
    // 세션 연장
    Redis::expire("session:" . $sessionId, 3600);
    
    return response()->json([
        "status" => "success",
        "user" => $userData,
        "active_sessions_count" => Redis::scard("active_sessions")
    ]);
});

// 실시간 통계
Route::get("/stats", function () {
    $stats = [];
    
    // MySQL 통계
    $stats["mysql"] = [
        "total_users" => DB::table("users")->count(),
        "tables" => count(DB::select("SHOW TABLES"))
    ];
    
    // Redis 통계
    $stats["redis"] = [
        "total_keys" => Redis::dbsize(),
        "active_sessions" => Redis::scard("active_sessions"),
        "memory_usage" => Redis::info("memory")["used_memory_human"]
    ];
    
    return response()->json($stats);
});
EOF'

---

PHP 문법 검사

docker compose exec php-fpm php -l routes/web.php

라우트 캐시 클리어

docker compose exec php-fpm php artisan route:clear

라우트 목록 확인

docker compose exec php-fpm php artisan route:list

테스트

curl -s http://localhost/system-status | jq .
curl -s http://localhost/test-integrated | jq .

 

728x90
반응형