1. Alpine을 쓰는 이유
표준 Debian/Ubuntu 기반 PHP 이미지는 크기가 400~700MB입니다. Alpine Linux 기반은 5MB 베이스에 필요한 것만 추가하면 최종 이미지가 80~120MB 수준입니다. CI/CD 빌드 시간과 레지스트리 저장 비용이 크게 줄어듭니다.
2. Dockerfile 기본 구조
FROM php:8.2-fpm-alpine
# Alpine 패키지 설치 (bash 없음, /bin/sh 사용)
RUN apk add --no-cache \
nginx \
nodejs npm \
git curl \
# PHP 익스텐션 의존성
libpng-dev jpeg-dev libwebp-dev \
oniguruma-dev icu-dev \
libzip-dev \
mysql-client
# PHP 익스텐션
RUN docker-php-ext-configure gd --with-jpeg --with-webp \
&& docker-php-ext-install \
pdo_mysql bcmath gd intl zip opcache \
mbstring pcntl
# Composer 설치 (공식 이미지에서 복사)
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# PHP 최적화 설정
COPY docker/php.ini /usr/local/etc/php/conf.d/app.ini
# 앱 코드 복사 & 빌드
COPY . .
RUN composer install --no-dev --optimize-autoloader \
&& npm ci && npm run build \
&& rm -rf node_modules
# 퍼미션 설정
RUN chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R 775 storage bootstrap/cache
CMD ["/bin/sh", "-c", "php-fpm -D && nginx -g 'daemon off;'"]
3. Alpine 주의사항 — 빠지는 함정들
bash 없음 → /bin/sh 사용
# ❌ bash 없어서 실행 불가 RUN /bin/bash -c "source .env" # ✅ /bin/sh 사용 RUN /bin/sh -c "cat .env | grep APP_KEY" # proc_open 에서도 /bin/bash 지정하면 오류 // ❌ Laravel에서 bash 경로 지정하지 않기 $process = new Process(['/bin/bash', '-c', 'ls']); // ✅ sh 사용 또는 직접 명령 $process = new Process(['ls', '-la']);
npm이 nodejs와 별개
# ❌ nodejs만 설치하면 npm 없음 RUN apk add nodejs # ✅ nodejs와 npm 둘 다 명시 RUN apk add nodejs npm # 또는 특정 버전 고정 RUN apk add nodejs=20.x npm
PostgreSQL 드라이버 체크
// ❌ 'postgres'만 체크 — 실제로 'pgsql'을 사용
if ($driver === 'postgres') { ... }
// ✅ 두 가지 모두 체크
if ($driver === 'postgres' || $driver === 'pgsql') { ... }
4. Nginx + PHP-FPM 설정
# docker/nginx.conf
server {
listen 80;
root /var/www/html/public;
index index.php;
# 정적 파일 캐시
location ~* \.(css|js|jpg|png|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
# 보안 헤더
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
}
5. docker-compose.yml
services:
app:
build: .
ports:
- "80:80"
environment:
APP_ENV: production
APP_KEY: ${APP_KEY}
DB_HOST: db
volumes:
- ./storage/logs:/var/www/html/storage/logs
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- db_data:/var/lib/mysql
- ./database/sql:/docker-entrypoint-initdb.d # SQL 자동 실행
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
db_data:
6. GitHub Actions CI/CD
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp:latest .
- name: Push to Registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker tag myapp:latest ghcr.io/myorg/myapp:latest
docker push ghcr.io/myorg/myapp:latest
- name: Deploy to Server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ghcr.io/myorg/myapp:latest
docker compose up -d --force-recreate app
docker exec myapp php artisan config:cache
docker exec myapp php artisan route:cache
docker exec myapp php artisan view:cache