왜 티스토리는 기본모드에서 yaml 코드블록을 지원하지 않는가..[물론 마크다운으로 하면 되는데 그러면 설정이 깨짐;;]
지난 포스팅에서는 docker-compose를 이용해 Node.js + React 개발 환경을 도커로 구성했다.
그런데 사실 Node.js는 도커 없이 직접 설치한다고 해도 그렇게 어렵지는 않다.
하지만 PHP, Laverel은 이야기가 조금 다르다.
- PHP는 Interperter일 뿐이다. PHP 혼자서는 HTTP Request를 처리할 수 없다.
- 웹 서버가 별도로 필요하다. Nignx, Apache 같은 웹 서버가 요청을 받아 PHP에게 넘겨줘야 비로서 코드가 실행된다.
- 설치 과정이 복잡하다.
- 만약 PHP + Nginx + MySQL을 각각 버전에 맞춰 설정한다고 해보자.
- 나 혼자 컴퓨터에서 하는 건 상관이 없겠지만, 다른 개발자와 프로젝트를 한다고 했을 때 설정이 꼬이거나 버전이 안맞는 문제 등이 나올 수 있따.
따라서 이번 시간 PHP, DB를 설치하지 않고, 도커 컨테이너들의 조합만으로 Lavarel 개발 환경을 구축해보고자 한다.
사실 나는 iOS, Spring 관련 학습만 했기에 이러한 프레임워크들은 처음 들어보았다.
여전히 PHP, Lavarel은 실무에서 사용된다고 한다.
나의 목표는 다음과 같다.
소스 코드는 내 컴퓨터에서 작성하지만, 실행 환경은 도커에 두는 것이다.
컨테이너들의 구성은 아래와 같다.

이 컨테이너들은 두 가지 그룹으로 나눌 수 있다.
- 애플리케이션 그룹: 항상 실행됨
- Nginx: 웹 서버, 요청이 들어오면 PHP 컨테이너로 넘긴다.
- PHP Interpreter: Lavarel 코드 해석 후 실행한다.
- MySQL: DB
- 유틸리티 그룹: 필요할 때만 실행됨
- Composer: PHP의 패키지 관리자다. Node.js의 npm, Swift에서 SPM과 같다.
- Artisan: Laravel 전용 명령어 도구다. DB migration, initial setting을 담당한다.
- NPM: JS/CSS 빌드용
docker-compose.yaml 작성
우선 6개의 컨테이너를 관리하기 위해 docker-compose.yaml 파일을 만들어보자.
╭─░▒▓ ~/Downloads/docker-complete ▓▒░·········░▒▓ ✔ system 19:29:35 ▓▒░─╮
╰─ tree ─╯
.
└── docker-compose.yaml
docker-compose의 outline을 작성해보자.
# version: "3.8" deprecated
services:
server:
php:
mysql:
composer:
artisan:
npm:
Nginx 컨테이너
가장 먼저 외부 요청을 받아들일 웹 서버를 설정해보자.
services:
server:
image: 'nginx:stable-alpine'
ports:
- '8000:80'
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
# php:
# mysql:
# composer:
# artisan:
# npm:
google에 docker nginx를 검색하면 설정법을 볼 수 있다.
https://hub.docker.com/_/nginx
nginx - Official Image | Docker Hub
Quick reference Supported tags and respective Dockerfile links 1.29.3, mainline, 1, 1.29, latest, 1.29.3-trixie, mainline-trixie, 1-trixie, 1.29-trixie, trixie 1.29.3-perl, mainline-perl, 1-perl, 1.29-perl, perl, 1.29.3-trixie-perl, mainline-trixie-perl
hub.docker.com
하나씩 봐보자.
- Image - nginx:stable-alpine
- ports
- 80: Nginx 컨테이너 내부에서 웹 서버가 열고 있는 포트
- 8000: 내 컴퓨터에서 접속할 포트
- 브라우저에서 localhost:8000으로 접속 시, 도커가 이를 컨테이너의 80번 포트로 연결해준다.
- volumes
- 기본 Nginx 설정 대신, 작성한 설정 파일(nginx.conf)을 컨테이너에 주입하는 과정이다.
- ./nginx/nginx.conf: 내 컴퓨터(Host)에 있는 설정 파일 경로
- /etc/nginx/con.d/default.conf: 컨테이너 내부의 Nginx 설정 파일 경로
- 컨테이너가 실수로 설정 파일을 수정하지 못하도록 read-only 지정
nginx.conf?
Nginx가 요청을 받았을 때 어떻게 처리해서 누구에게 넘길지 정의해주는 파일이다.
server {
listen 80; # 컨테이너 내부 80번 포트 수신
index index.php index.html; # 기본 파일 우선순위
server_name localhost;
root /var/www/html/public; # 나중에 소스 코드가 위치할 경로
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ { # .php로 끝나는 요청이 오면
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:3000; # 'php' 서비스의 3000번 포트로 넘긴다
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
- root /var/www/html/public
- Laravel 프로젝트의 시작점(index.php)이 위치할 폴더
- 지금 당장은 이 경로에 파일이 없지만, 나중에 PHP 컨테이너와 소스 코드를 공유(Volume Mount) 할 때 이 경로를 기준으로 맞추게 된다.
- fastcgi_pass php:3000
- Nginx는 PHP 코드를 해석할 줄 모른다.
- 따라서 PHP 파일 요청이 오면, php라는 이름을 가진 컨테이너(서비스)에게 처리를 위임한다.
- 두 컨테이너가 서로 통신하는 연결고리: php
PHP 컨테이너
공식 PHP Image도 있지만, 이걸로만 Laravel 프레임워크를 돌릴 수는 없다.
- Lavarel이 필요로 하는 특정 확장 프로그램(PDO, MySQL 관련)이 기본 이미지에는 없기 떄문이다.
따라서 필요한 기능들을 추가해서 직접 빌드해보자.
프로젝트 루트에 dockerfiles라는 폴더를 만들고, 그 안에 php.dockerfile을 생성한다.

FROM php:8.2-fpm-alpine
WORKDIR /var/www/html
RUN docker-php-ext-install pdo pdo_mysql
여기서 중요한 점이 있다.
- WORKDIR /var/www/html
- 컨테이너 내부에서 소스 코드가 위치할 표준 경로
- 앞으로 모든 명령은 이 폴더를 기준으로 실행된다.
- 참고로 /var/www/html은 리눅스 서버 세계에서 Web Root라고 불리는 상징적인 경로라고 한다.
- 해당 프로젝트에서는 '내 컴터/Nginx 컨테이너/PHP 컨테이너'가 이 경로를 통해 '동기화'된다.
- 내 컴터에서 src라는 폴더에 코드를 짠다면 컨테이너들이 자기들의 '/var/www/html'에 코드를 마운트한다.
- nginx.conf에도 root /var/www/html/public; 부분이 있는데
- /var/www/html에 전체 소스 코드를 넣어도
- Nginx는 /public 폴더만 보라고 시키는 거다.
- RUN 밖에 없는데?
- CMD가 없으면 베이스 이미지가 가지고 있는 CMD를 실행하게 되는데
- 이때 php:fpm이 'php-fpm'이라는 실행 명령을 가지고 있으므로 따로 적지 않아도 PHP 인터프리터가 실행된다.
작성한 Dockerfile을 바탕으로 docker-compose.yaml에 php 서비스를 정의한다.
services:
server:
# ... (이전 단계의 Nginx 설정 유지) ...
php:
# 1. 빌드 설정
build:
context: ./dockerfiles # Dockerfile이 있는 폴더 위치
dockerfile: php.dockerfile # 사용할 파일 이름
# 2. 소스 코드 공유
volumes:
# 내 컴퓨터의 ./src 폴더를 컨테이너의 /var/www/html에 마운트
# :delegated -> 쓰기 속도 최적화 옵션 (컨테이너의 쓰기 작업이 호스트에 반영되는 걸 살짝 지연시켜 성능 향상)
- ./src:/var/www/html:delegated
# 3. 포트 설정
# Nginx 컨테이너가 내부망을 통해 직접 9000번으로 접속하므로,
# 호스트(내 컴퓨터)로 포트를 뺄 필요(ports:)가 없다.
실제 PHP 컨테이너 공식 이미지는 내부적으로 9000번 포트를 연다.
- 우리는 Nginx 초기 설정에서 fastcgi_pass php:3000을 작성해 3000번으로 보낸다고 작성했다.
즉, Nginx는 3000번으로 문을 두드리는데, PHP는 9000번에서 기다리고 있으니 연결이 안된다.
어 그럼 docker-compose의 ports 옵션으로 해결할 수 있을 거 같지만..!
- ports는 내 컴퓨터와 연결할 때 쓰는 것이지, 컨테이너 간 내부 통신에는 영향을 주지 않는다.
따라서 nginx.conf 파일을 수정해 PHP 실제 포트인 9000번을 바라보게 한다.
nginx.conf 수정
location ~ \.php$ {
# ... (생략)
fastcgi_pass php:9000; # 3000 -> 9000 으로 수정
# ... (생략)
}
MySQL 컨테이너
PHP 애플리케이션이 데이터를 저장할 저장소를 만들고, 보안/설정을 위해 .env를 정의해보자.
services:
server:
image: "nginx:stable-alpine"
ports:
- "8000:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
php:
build:
context: ./dockerfiles
dockerfile: php.dockerfiles
volumes:
- ./src:/var/www/html:delegated
mysql:
image: "mysql:8.0"
env_file:
- ./env/mysql.env
# composer:
# artisan:
# npm:
왜 ports를 설정 안하냐면
- 도커 컴포즈가 자동으로 이들을 하나의 네트워크에 묶어주기 때문이다.
- PHP 컨테이너는 이제 mysql이라는 이름(서비스명)만으로 이 데이터베이스를 찾아갈 수 있다.
- etc: 접속 호스트 주소가 127.0.0.1이 아니라 mysql이 된다.
MySQL 컨테이너는 처음 실행될 때 "초기 데이터베이스 이름은 뭘로 할지", "비밀번호는 뭘로 할지"를 알기 위해 특정 환경 변수를 확인한다. 이 내용을 별도의 파일로 관리한다.

MYSQL_DATABASE=foden
MYSQL_USER=foden
MYSQL_PASSWORD=psg
MYSQL_ROOT_PASSWORD=psg
위와 같이 env를 작성해주자.
Composer 컨테이너
기본 Composer 이미지를 그대로 쓰지 않고, 옵션 추가을 위해 커스텀 이미지를 만든다.
dockerfiles 폴더 안에 composer.dockerfile을 생성한다.
FROM composer:latest
# 다른 컨테이너들과 똑같이 '/var/www/html'을 작업 공간으로 맞춥니다.
WORKDIR /var/www/html
# --ignore-platform-reqs: 컴퓨터 환경 신경 쓰지 말고 설치하라는 옵션
ENTRYPOINT [ "composer", "--ignore-platform-reqs" ]
이제 docker-compose.yaml에 composer 서비스를 추가한다.
services:
server:
image: "nginx:stable-alpine"
ports:
- "8000:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
php:
build:
context: ./dockerfiles
dockerfile: php.dockerfile
volumes:
- ./src:/var/www/html:delegated
mysql:
image: "mysql:8.0"
env_file:
- ./env/mysql.env
composer:
build:
context: ./dockerfiles
dockerfile: composer.dockerfile
volumes:
- ./src:/var/www/html
# artisan:
# npm:
왜 Composer를 사용하는 지 자세히 알아보자.
- 현재 server, php, mysql 컨테이너를 다 준비했지만, 정작 실행할 Laravel 코드가 없다.
- 보통은 내 컴퓨터 터미널에서 composer create-project ... 를 쳐서 코드를 다운받지만, 내 컴퓨터에 Composer를 설치하지 않기로 했다.
그래서 Composer가 설치된 일회용 컨테이너를 잠시 사용해서, 내 컴퓨터의 src 폴더에 Laravel 코드를 다운로드만 하는 용도로 사용한다.
추가로, 볼륨 마운트 설정을 한 덕분에 Composer 컨테이너가 내부 /var/www/html에 Laravel 파일을 다운로드하면, ./src 폴더(내 컴터)에도 똑같이 파일이 생긴다. 그 후 php, server 컨테이너가 이 파일을 읽어서 웹사이트를 띄우게 된다.
이제 Laravel 소스 코드를 다운로드 받아보자.
docker-compose run --rm composer create-project --prefer-dist laravel/laravel .

그럼 마운트 시켰던 src 폴더에 파일들이 주루룩 다운받아진다.
애플리케이션 실행
Composer 컨테이너가 만들어준 Laravel 프로젝트 안에 .env를 수정해야 된다.
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=foden
DB_USERNAME=foden
DB_PASSWORD=psg
그리고 매번 docker-compose up server php mysql 을 다 치긴 귀찮으니 의존성을 설정해주자.
services:
server:
# ...
depends_on:
- php
- mysql
docker compose up server 를 하게 되면 localhost:8000으로 웹사이트 접속이 가능하다.


localhost 접속시 아래와 같은 사이트가 뜬다.

다른 Utility 컨테이너 생성
남은 artisan, npm 컨테이너를 만들어보자.
- artisan은 Laravel 프로젝트를 관리하는 명령어 도구다.
- etc: DB Table 생성, 컨트롤러 생성 등
- PHP 위에서 돌아가는 스크립트이므로, 굳이 새로운 이미지를 만들지 않고 php.dockerfile을 재사용한다.
- 하지만, 기존 PHP 컨테이너와는 다른 명령을 수행할 것이 있다.
이런 다른 명령을 docker-compose.yaml에 정의한다.
services:
# ... (기존 server, php, mysql, composer) ...
artisan:
# 1. 빌드 설정 (PHP 이미지 재사용)
build:
context: ./dockerfiles
dockerfile: php.dockerfile
# 2. 소스 코드 공유
volumes:
- ./src:/var/www/html
# 3. Entrypoint 오버라이드
# Dockerfile에 설정이 없거나 있더라도, 여기서 덮어씌운다.
# php로 /var/www/html/artisan 파일을 실행하라는 뜻
entrypoint: ["php", "/var/www/html/artisan"]
npm 컨테이너도 설정해주자.
이번에는 dockerfile을 따로 만들지 않고, 도커 컴포즈 안에 모든 설정을 처리해보자.
services:
# ... (기존 서비스들) ...
npm:
image: 'node:lts-alpine'
working_dir: /var/www/html
# Entrypoint 설정
entrypoint: ["npm"]
volumes:
- ./src:/var/www/html
이제 6개의 컨테이너가 모두 정의되었으니, 테스트를 해보자.
- artisan 컨테이너를 시켜 MySQL에 데이터를 써보자.


지금까지는 volumes를 통해 내 컴퓨터의 코드를 컨테이너와 실시간으로 공유했다.
하지만, 실제 서버에 배포할 때는 내 컴터는 없으므로, 코드 자체가 이미지 안에 Snapshot이 되어 있어야 한다.
Dockerfile을 수정하고 build Context를 조정해보자.
nginx.dockerfile 수정
기존에는 nginx 이미지를 그대로 썼지만, 이제는 설정 파일, 소스 코드를 이미지 안에 COPY 해 넣어야 한다.
FROM nginx:stable-alpine
# 1. Nginx 설정 파일 복사 및 이름 변경
# 호스트의 nginx.conf를 컨테이너 설정 경로로 복사한다.
WORKDIR /etc/nginx/conf.d
COPY nginx/nginx.conf /etc/nginx/conf.d/
# Nginx가 인식할 수 있도록 파일 이름을 default.conf로 변경한다.
RUN mv nginx.conf default.conf
# 2. 소스 코드 복사 (Snapshot)
# 배포 시 바인드 마운트 없이도 정적 파일을 서빙할 수 있도록 코드를 내장한다.
WORKDIR /var/www/html
COPY src .
php.dockerfile 수정
# 최신 버전인 PHP 8.2 사용
FROM php:8.2-fpm-alpine
# 1. 소스 코드 복사 (Snapshot)
WORKDIR /var/www/html
COPY src .
RUN docker-php-ext-install pdo pdo_mysql
# 3. 파일 소유권 변경
# COPY로 복사된 파일은 기본적으로 root 소유가 된다.
# Laravel이 로그나 캐시를 쓸 수 있도록, 파일 주인을 PHP 실행 유저(www-data)로 변경한다.
RUN chown -R www-data:www-data /var/www/html
Docker Compose build context 수정
Dockerfile 내부에서 COPY src . 명령을 사용했다. src 폴더는 dockerfiles 폴더 밖에 위치한다.
기존 설정대로라면 dockerfiles 폴더 안에서만 파일을 찾으려 하므로 에러가 발생한다.
따라서 빌드를 실행하는 context를 프로젝트 최상위로 변경해야 한다.
- 변경 후 배포 시에도 잘 작동하는지 확인하기 위해 volumes를 주석처리한다.
services:
server:
# image: "nginx:stable-alpine"
build:
context: .
dockerfile: dockerfiles/nginx.dockerfile
ports:
- "8000:80"
# volumes:
# - ./src:/var/www/html
# - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
- mysql
php:
build:
context: .
dockerfile: dockerfiles/php.dockerfile
# volumes:
# - ./src:/var/www/html:delegated
mysql:
image: "mysql:8.0"
env_file:
- ./env/mysql.env
composer:
build:
context: ./dockerfiles
dockerfile: composer.dockerfile
volumes:
- ./src:/var/www/html
artisan:
build:
context: .
dockerfile: dockerfiles/php.dockerfile
volumes:
- ./src:/var/www/html
entrypoint: ["php", "/var/www/html/artisan"]
npm:
image: node:14
working_dir: /var/www/html
entrypoint: ["npm"]
volumes:
- ./src:/var/www/html
이제 컨테이너를 실행하면 내 컴터의 src 폴더가 아니라, 이미지 빌드 시점에 copy된 코드로 실행한다.
- 다시 docker-compose up --build server를 할 시 사이트가 잘 뜬다.

이 과정을 정리해보면
- 이미지 빌드 시 (COPY)
- 소스 코드의 스냅샷(현재 상태)이 이미지 안에 저장된다.
- 이 이미지를 그대로 서버에 가져가면(배포), 별도의 설정 없이 바로 앱이 실행된다.
- 컨테이너 실행 시 (Volumes)
- 개발 중에는 docker-compose의 볼륨 설정이 COPY된 파일 위를 덮어쓴다.
- 개발자는 코드를 고치고 새로고침하면 바로 반영된다.
정리
이런 환경을 구두로 공유를 한다면..?
IaC가 이래서 강조되는 것 같기도 하다.
- docker-compose.yaml, dockerfiles만 전달하면 개발 환경 세팅을 동일하게 할 수 있다...

'Infra > Docker' 카테고리의 다른 글
| Container의 근반이 되는 기술에 대해 알아보자(완) UnionFS (0) | 2025.12.11 |
|---|---|
| Container의 근반이 되는 기술에 대해 알아보자(2) cgroup (0) | 2025.12.11 |
| Container의 근반이 되는 기술에 대해 알아보자(1) namespace (0) | 2025.12.11 |
| Utility Docker에 대해 알아보자 (0) | 2025.12.07 |
| Docker Compose (0) | 2025.12.06 |