클라우드의 리눅스 가상서버에 직접 구축한 워드프레스 블로그의 방문자 분석을 위해 직접 PHP로 코딩하여 사용하는 웹사이트가 있다. 이 웹사이트는 직접 설치한 Apache + PHP-FPM + MariaDB에 직접 코딩한 PHP 코드로 작성하였고 1년 넘게 잘 사용하고 있다.
그리고 계속 조금씩 코드를 수정해 현재의 모습이다.

이것도 병이라면 병일까? 어느 날 갑자기 이 웹사이트를 컨테이너화 해볼까? 라는 충동에 휩싸였다. 최근에 이것저것 컨테이너 기반의 유틸리티 서버를 컨테이너로 하나의 서버에 구축하고 있는데 이 워드프레스 로그 분석 사이트도 컨테이너로 만들어 함께 올리면 효과적이겠다는 생각이 들었다. 하고 싶다는 마음과 귀차니즘이 일주일 정도 격돌한 끝에 작업에 돌입했다.
CI/CD 파이프라인
이 워드프레스 블로그 접속기록 분석 사이트는 엄밀하게 이야기하면 워드프레스가 실행되고 있는 Apache 웹서버의 access.log 파일을 읽어 분석하는 PHP 프로그램이 실행중인 웹사이트다. 지금까지 이 워드프레스 블로그 접속기록 분석 웹사이트를 관리하던 CI/CD 파이프라인을 설명하자면 다음과 같다.
웹사이트의 PHP 소스코드는 Github의 worpress-log 라는 리포지토리에 저장되어 있다. 리포지토리에 저장된 소스코드의 수정을 위해서 노트북에 Git을 설치한 다음 Github의 worpress-log 리포지토리를 복제(Clone)했다. 그리고 Visual Stuido Code (VSCode)로 코딩하고 있다.
소스코드의 수정은 노트북PC에 설치된 VSCode에서 수정하고 노트북 PC의 Git에 Commit 한다. 수정이 완료되면 Github의 wordpress-log 리포지토리의 개발 브랜치에 로컬 Git에 저장된 새 코드를 Push하고 main 브랜치에 Merge한다. Merge가 되면 Github Actions에 정의된 Workflow에 의해 웹서버에 SSH 로 접속해 코드가 자동으로 배포된다. 이 과정이 바로 CI/CD 파이프라인이다.
이런 웹사이트를 컨테이너로 포팅하고 효과적으로 관리하기 위해서는 CI/CD 파이프라인을 구축해야 한다. CI/CD 파이프라인을 구축하는 방법은 매우 다양하다.
필자가 선택한 방식은 다음과 같다.
Github의 리포지토리는 그대로 사용하지만 코드를 컨테이너 이미지로 빌드하여 도커가 설치된 서버에 배포하는 것은 Github Actions가 아닌 지금 사용하고 있는 Portainer의 컨테이너 Deploy 기능을 사용하기로 했다. Portainer는 컨테이너의 집합체인 Stack를 Deploy할 때 Github의 리포지토리를 접근할 수 있는 기능을 지원하기 때문에 NginX의 Dockerfile과 php-fpm의 Dockerfile 그리고 docker-compose.yml 파일을 Github에 생성해두고 Portainer에서 Github의 ID와 PAT 토큰 그리고 리포지토리 URL을 지정해주면 알아서 Dockerfile과 docker-compose.yml 파일을 내려받아 컨테이너로 빌드한 다음 디플로이(Deploy)해 준다.
그 과정을 설명한다.
깃허브 리포지토리 구조화
먼저 Github의 리포지토리에 코드들을 체계적인 디렉토리 구조로 변경하고 컨테이너 빌드에 필요한 파일들을 추가해야 한다.

먼저 컨테이너로 빌드할 소스코드와 Dockerfile 그리고 docker-compose.yml 파일을 앞의 이미지와 같이 구조화한다. 리포지토리의 이름은 wordpress-log 다. 이 리포지토리의 최상위 경로에 위 화면과 같이 nginx/ 디렉토리와 php-fpm/ 디렉토리를 생성한다. 그리고 두 디렉토리 아래에 각각 소스코드가 저장될 public/ 디렉토리를 생성한다.
그리고 nginx/ 디렉토리에는 Poirtainer가 nginx 컨테이너를 빌드할 때 사용할 Dockerfile과 nginx 컨테이너가 사용할 nginx.conf 파일을 생성하게 되고 nginx/public 디렉토리에는 php-fpm에서 처리되지 않는 일반 html 파일이나 css 파일 그리고 js 파일들을 저장한다.
다음으로 php-fpm/ 디렉토리에는 Portainer가 php-fpm 컨테이너를 빌드할 때 사용할 Dockerfile을 생성하고 php-fpm/public 디렉토리에는 php-fpm이 처리할 php 파일들을 위치시키면 된다.
nginx/public과 php-fpm/public 디렉토리에 있는 파일들의 목록이다. nginx/public/ 디렉토리에는 main.css 파일이 있는 것도 확인할 수 있다.

NginX 컨데이너 빌드에 사용될 Dockerfile
nginx/ 디렉토리에 다음 코드를 Dockerfile로 저장한다. nginx 컨테이너 이미지를 빌드할 때 사용되는 코드다. 자세한 설명은 주석으로 대신한다.
FROM nginx:stable
# 리포지토리에 있는 nginx.conf 파일을 컨테이너의 /etc/nginx/nginx.conf 로 복사
# 리포지토리의 현재 디렉토리는 Dockerfile이 있는 nginx/ 임
COPY nginx.conf /etc/nginx/nginx.conf
# 작업디렉토리를 컨테이너의 /var/www/html로 변경. cd 명령이라고 보면 됨
WORKDIR /var/www/html
# 리포지토리의 nginx/ 아래의 public/에 있는 모든 파일을 컨테이너의 /var/www/html 디렉토리로 모두 복사함
COPY public/ .
# nginx 컨테이너가 80번을 노출시킨다는 의미. 그냥 알려주는 것임. nginx.conf의 웹서버 포트에서 지정한 포트를 적어주면 됨
EXPOSE 80
이 파일로 NginX 컨테이너가 빌드된다. 아래가 리포지토리에 저장된 nginx의 Dockerfile 화면이다.

nginx 컨테이너에 삽입할 nginx.conf 파일
앞의 nginx용 Dockerfile에서 COPY 명령으로 리포지토리에서 컨테이너 이미지 내부로 복사하는 nginx.conf 파일을 Dokerfile과 같은 nginx/ 디렉토리에 생성한다.
# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
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;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
gzip on;
server {
listen 80;
server_name localhost;
root /var/www/html; # PHP 소스코드가 있는 경로
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
# Nginx 컨테이너에서 php-fpm 컨테이너의 9000번 포트로 요청 전달
fastcgi_pass php-fpm:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# Hide .env files
location ~ /\.env {
deny all;
}
# Hide dot files (e.g., .git)
location ~ /\. {
deny all;
}
}
}
마찬가지로 더 이상 자세한 nginx.conf에 대한 설명은 생략한다.
php-fpm 컨테이너 빌드에 사용될 Dockerfile
php-fpm 컨테이너 빌드에 사용될 Dockerfile을 php-fpm/ 디렉토리에 생성한다.
FROM php:8.3-fpm
# php-fpm 컨테이너에 mysql에 접속하기 위한 패키지 등을 설치해준다. mysqli가 없으면 mysql 또는 mariadb에 접속할 때 에러가 발생한다.
RUN docker-php-ext-install pdo pdo_mysql mysqli
# 컨테이너의 작업디렉토리를 /var/www/html로 지정한다.
WORKDIR /var/www/html
# Dockerfile이 있는 php-fpm/ 디렉토리 아래의 public/ 아래에 있는 모든 파일(소스코드 파일)을 /var/www/html 로 모두 복사한다.
COPY public/ .
# php-fpm 기본포트가 9000이므로 9000번을 사용함을 알려준다.
EXPOSE 9000
docker-compose.yml 생성
wordpress-log 리포지토리의 최상위 디렉토리에 다음과 같이 docker-compose.yml을 작성한다.
version: "3.8"
services:
nginx: # nginx 빌드 시작.
build:
context: ./nginx # 리포지토리 최상단에 있는 nginx 디렉토리 내에 있는 Dockerfile을 참조한다는 의미다.
container_name: wordpress-log-nginx
expose:
- "80"
environment:
- TZ=Asia/Seoul # 타임존을 Seoul로 지정한다. 없으면 UTC로 시간이 출력될 가능성이 높다.
networks:
- reverse_proxy # nginx 컨테이너가 사용할 도커 네트워크 인터페이스. 컨테이너의 80을 호스트의 특정 포트를 통해 직접 노출시키지 않고 Reverse_proxy인 Caddy를 통해 간접 노출시킨다. reverse_proxy는 Caddy에서 사용하는 도커 네트워크이고 nginx도 동일한 도커 네트워크에만 연결하여 Caddy에게 wordpress-log-nginx 컨테이너의 80으로 전달하도록 뒤에서 구성한다.
depends_on:
- php-fpm # php-fpm이 먼저 시작된 다음 wordpress-log-nginx 컨테이너가 실행되도록 해준다. 의존성 키워드라고 보면된다.
php-fpm:
build:
context: ./php-fpm
container_name: wordpress-log-php-fpm
volumes:
- /var/www/html/logs:/var/www/html/logs # 로그 분석에 사용될 호스트의 log파일 경로를 컨테이너에 마운트 한다. 즉 host의 /var/www/html/logs를 컨테이너의 /var/www/html/logs에 연결해주는 설정
environment:
- TZ=Asia/Seoul # 시간을 서울로 맞춰준다.
networks:
- reverse_proxy # nginx와 동일한 네트워크에 연결해줘야 php 파일처리 요청을 받을 수 있다.
networks:
reverse_proxy:
external: true # nginx와 php-fpm 컨테이너가 사용하는 reverse_proxy 네트워크는 이미 만들어져 있는 Docker Network임을 알려줌. 만약 없으면 docker-compose가 reverse_prox 네트워크를 생성함
이 docker-compose.yml을 생성한 화면이다.

이제 Guithub에 있는 Repository 의 구조화와 컨테이너 이미지 빌드에 필요한 파일의 생성은 완료되었다.
Portainer에서 Github 접근에 필요한 PAT 토큰 생성
만약 Guithub의 리포지토리가 Private 이라면 Portainer에서 Stack을 생성하기 위해 docker-compose.yml과 두 개의 Dockerfile 그리고 nginx.conf 파일과 웹사이트 소스코드에 접근할 때 사용자 인증정보를 Portainer에게 알려줘야 한다. 그 때 사용되는 인증정보는 Github의 사용자 ID와 PAT 토큰이다. 당연히 Github에서 PAT 토큰을 생성해야 한다.
Github에 로그인한 다음 사용자 계정의 보라색 아이콘을 누르면 메뉴가 표시되고 “Settings”를 클릭한다.

Settings 메뉴에 들어가면 왼쪽 하단에 있는 “Developer settings” 메뉴를 클릭한다.

그리고 “Personal access tokens” – “Tokens(Classic)” 을 선택하고 오른쪽의 “Generate new token” 을 선택한다.

일반적인 용도의 “Generate new tokens (classic)” 을 선택한다.

Note에 토큰의 용도 등을 적어넣은 다음 사용 기한을 선택한다. 여기서는 기한 없이 무기한 사용할 수 있는 “No expiration”을 선택했다.

그리고 접근범위를 지정하게 되는데 “repo”에 체크를 했다.
완료하면 다음과 같이 연한 초록색 영역에 PAT 토큰을 보여준다. 그런데 이 창을 닫으면 다시 볼 수 없다. 즉 한번 발급된 토큰은 다시 확인이 불가하므로 주의하자.

이 PAT 토큰을 잘 기억(?)해두자 Portainer에서 Stack을 빌드할 때 Github의 리포지토리 접근 시 인증 값으로 설정해줘야 한다.
Github 리포지토리 주소(URL)
Portainer에서 Stack을 생성할 때 사용자 ID와 PAT 토큰 그리고 하나 더 필요한 정보가 바로 Github Repository URL이다. 이 URL은 다음과 같이 Github의 리포지토리에 가면 쉽게 확인할 수 있다.

이제 포테이너에서 Stack을 빌드할 준비가 완료되었다. Portainer에서 Stack은 하나 이상의 컨테이너를 포함하는 컨테이너 관리 집합이라고 생각하면 된다. 여기에서 빌드하는 nginx와 php-fpm 컨테이너 두 개를 하나의 Stack으로 관리할 수 있다.
Portainer에서 Stack 생성하기
Portainer에서 스택을 생성할 Environment를 선택하고 “Stacks” 메뉴에 진입한다.

Stacks 화면에 진입한 다음 “Add stack” 버튼을 선택하면 다음과 같이 Create Stack 화면이 보이는데 세 번째의 “Repository”를 선택한다.

Git Repository의 Authentication은 Github에 있는 Repository가 Private인 경우에만 활성화하면 된다. 그리고 Github의 ID와 앞에서 생성한 PAT 토큰을 입력해준다.
그리고 Repository의 URL을 입력해준 다음 화면 아래로 스크롤 한 다음 “Deploy the stack”을 클릭한다. 정상적으로 Stack이 생성되지 않으면 무엇인가 설정상 오류가 있는 것이다. 정상적으로 생성되면 다음과 같이 Stacks 목록에 표시된다.

생성된 Stack을 클릭해 상세 정보를 확인하면 다음과 같이 두 개의 컨테이너가 생성되어 Running 중임을 확인할 수 있다.

Caddyfile에 도메인주소와 NginxX 컨테이너 연결하기
NginX와 php-fpm이 정상적으로 실행되었다면 다음과 같이 리버스 프록시인 Caddy에 NginX 컨테이너와 연결해줄 설정을 추가해준다.

Caddy는 Reverse Proxy로서 Caddy가 호스트에서 Listen하고 있는 포트로 유입되는 HTTP, HTTPS 트래픽에서 도메인주소(wplog.abc.co.kr 과 같은)에 따라 미리 정의된 컨테이너로 트래픽을 전달해주는 역할을 수행한다. 따라서 Caddyfile에서 위 와 같이 wplog.****.***.kr 주소로 들어오는 트래픽을 reverse_proxy 도커 네트워크에 연결되어 있는 nginx 컨테이너의 80번 포트로 전달한다.
그렇게 되면 컨테이너로 이전한 웹사이트가 동일하게 표시된다.
물론, PHP 버전과 환경이 달라지는 만큼 코드에서 이런저런 에러가 많이 발생했다. 그 에러 하나하나를 수정하면서 CI/CD 파이프라인을 안정화시키면 된다.
#CI/CD #워드프레스 #Containerization #docker_based_deploy
답글 남기기