레거시 Laravel 프로젝트를 처음 받았다. PostgreSQL 데이터베이스가 3개, 외부 API가 4개, PHP 확장 모듈이 잔뜩. README에는 “PHP 설치 후 php artisan serve”라고만 적혀 있다.
이런 프로젝트를 Mac에 직접 세팅하면 하루가 간다. Homebrew로 PHP 깔고, PostgreSQL 깔고, Redis 깔고, 버전 충돌 잡고… 그래서 Docker로 전부 컨테이너화했다. 결과적으로 docker compose up 한 줄이면 전체 환경이 뜬다.
이 글은 그 과정에서 터진 것들과 해결법을 기록한다.
최종 구조
프로젝트 루트에 Dockerfile, docker-compose.yml, 그리고 docker/ 디렉토리(Nginx 설정, PostgreSQL init 스크립트)를 추가했다.
5개 컨테이너가 하나의 Docker 네트워크에서 동작한다:
- app — PHP 8.4-FPM. Laravel 애플리케이션 실행.
- nginx — 리버스 프록시.
:8080으로 접근. - db — PostgreSQL 16. 하나의 인스턴스에 3개 DB를 생성.
- redis — 캐시/세션/큐.
- meilisearch — 전문 검색 엔진.
Dockerfile — PHP 8.4 + 확장 모듈 삽질
php:8.4-fpm 베이스 이미지 위에 PostgreSQL, Redis, intl, zip 확장을 설치하고, Composer와 Node.js 20을 추가한 구성이다. 의존성 레이어를 먼저 복사해서 Docker 캐시를 활용하고, 마지막에 소스 코드를 복사하는 패턴을 썼다.
삽질 #1: PHP 8.2 → 8.4
원래 프로젝트는 PHP 8.2였다. 그런데 composer install을 돌리면 Symfony 패키지들이 PHP 8.4 이상을 요구한다. Laravel 12가 의존하는 Symfony 7.x 컴포넌트들이 최소 요구 버전을 올렸기 때문이다. 에러 메시지에 symfony/http-kernel requires php >=8.4가 뜬다.
해결: 베이스 이미지를 php:8.4-fpm으로 변경. 끝. 하지만 이걸 모르면 의존성 지옥에서 한참 헤맨다.
삽질 #2: ext-intl, ext-zip 누락
PHP 공식 Docker 이미지에는 intl과 zip 확장이 기본 포함되어 있지 않다. 로컬 Mac에서는 Homebrew PHP에 번들되어 있어서 모르고 넘어가기 쉬운데, Docker에서 composer install을 돌리면 바로 터진다.
해결: libicu-dev, libzip-dev 시스템 패키지 설치 후 docker-php-ext-install intl zip 추가.
삽질 #3: phpredis
Laravel의 Redis 클라이언트로 phpredis를 쓰는 경우(.env에 REDIS_CLIENT=phpredis), PHP 확장으로 redis가 설치되어 있어야 한다. predis(순수 PHP 구현)와 달리 C 확장이라 pecl install redis로 별도 설치가 필요하다.
삽질 #4: Node.js 버전
Vite가 Node.js 18+을 요구한다. Docker의 Debian 기본 저장소에 있는 Node.js는 구버전이라 npm run build에서 실패한다. NodeSource 20.x 저장소를 추가하면 해결된다.
docker-compose.yml — vendor/node_modules 격리
5개 서비스(app, nginx, db, redis, meilisearch)를 정의한다. 여기서 가장 중요한 포인트는 볼륨 격리 패턴이다.
소스 코드를 .:/var/www로 마운트하면 호스트의 (비어 있는) vendor/가 컨테이너의 vendor/를 덮어쓴다. 이걸 방지하려면 vendor와 node_modules를 익명 볼륨으로 선언해서 호스트 마운트에서 제외해야 한다.
이 패턴을 모르면 “컨테이너 안에서 composer install 했는데 왜 autoload가 안 되지?”에서 한참 헤맨다.
Nginx 설정
표준 Laravel + PHP-FPM용 Nginx 설정이다. 핵심 포인트는 fastcgi_pass에서 IP 대신 Docker Compose 서비스 이름(app)을 쓴다는 것. Docker Compose 네트워크 안에서 서비스 이름이 DNS 호스트명으로 자동 등록된다.
멀티 DB 설정 — config/database.php
이 프로젝트의 핵심 난이도. 하나의 Laravel 앱에서 PostgreSQL 데이터베이스 3개를 동시에 사용한다.
config/database.php의 connections 배열에 커넥션 3개를 각각 정의하고, 각 커넥션마다 별도의 .env 변수 세트를 할당한다. 모델에서는 protected $connection = 'catalog'처럼 어떤 DB에 연결할지 명시한다.
삽질 #5: .env 변수 네이밍
DB가 3개이므로 .env 변수도 3세트가 필요하다. Laravel의 기본 DB_HOST, DB_DATABASE는 하나의 커넥션만 커버한다. 멀티 DB에서는 DB_PG_CATALOG_HOST, DB_PG_ACCOUNTS_HOST처럼 접두사로 구분해야 한다.
로컬 개발에서는 하나의 PostgreSQL 인스턴스에 CREATE DATABASE로 3개를 만들고, host를 모두 같은 Docker 서비스명(db)으로 가리키면 된다.
삽질 #6: 마이그레이션 충돌
마이그레이션은 기본 커넥션에서 실행된다. 레거시 테이블이 이미 init.sql에 정의되어 있으면 php artisan migrate에서 “table already exists” 에러가 난다.
해결: init.sql에서 마이그레이션이 관리하는 테이블(users, cache, jobs 등)은 제외하고, 레거시 전용 테이블만 포함한다. 마이그레이션과 init.sql의 관할 범위를 명확히 나누는 게 핵심이다.
PostgreSQL init.sql — Docker 초기화
Docker Compose에서 PostgreSQL 컨테이너가 처음 시작될 때, /docker-entrypoint-initdb.d/ 안의 SQL 파일을 자동 실행한다. 여기에 레거시 스키마를 넣으면 docker compose up 한 번에 3개 DB가 준비된다.
docker-compose.yml의 db 서비스 볼륨에 ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql을 마운트하면 된다.
삽질 #7: init.sql이 한 번만 실행됨
docker-entrypoint-initdb.d/의 스크립트는 데이터 볼륨이 비어 있을 때만 실행된다. init.sql을 수정했는데 반영이 안 되면 docker compose down -v로 볼륨을 날린 뒤 다시 올려야 한다.
-v 없이 down하면 볼륨이 남아서 init.sql 변경이 무시된다. 이걸 모르면 “SQL 수정했는데 왜 안 바뀌지?”에서 한참 헤맨다.
정리
레거시 Laravel 프로젝트를 Docker로 컨테이너화할 때 자주 터지는 포인트:
- PHP 버전 — Laravel 12 + Symfony 7.x는 PHP 8.4 필수
- 확장 모듈 —
intl,zip,phpredis는 Docker PHP 이미지에 없다. 직접 설치해야 한다 - vendor/node_modules 격리 — 익명 볼륨으로 호스트 마운트와 분리
- 멀티 DB —
.env변수 접두사로 커넥션 분리, 모델에서$connection지정 - init.sql 관할 — 마이그레이션이 관리하는 테이블과 레거시 테이블의 범위를 명확히 나누기
- init.sql 캐시 —
docker compose down -v로 볼륨 날리지 않으면 변경 반영 안 됨
Docker Compose 파일 하나와 Dockerfile 하나. 이걸 세팅해두면 팀원이 합류해도 docker compose up 한 줄이면 끝난다. 환경 세팅에 하루 쓰는 건 첫 번째 사람만으로 충분하다.