2 - tmp
This commit is contained in:
parent
a7036cda3d
commit
a5307f3e5d
13 changed files with 1818 additions and 40 deletions
63
.dockerignore
Normal file
63
.dockerignore
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.woodpecker.yml
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Development
|
||||||
|
.editorconfig
|
||||||
|
.eslintrc*
|
||||||
|
.prettierrc*
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# Claude files
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Node modules (will be installed in build)
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
|
||||||
|
# Backend vendor (will be installed in build)
|
||||||
|
vendor/
|
||||||
|
backend/vendor/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
frontend/dist/
|
||||||
|
frontend/build/
|
||||||
|
|
||||||
|
# Cache and logs
|
||||||
|
backend/storage/logs/*
|
||||||
|
backend/storage/framework/cache/*
|
||||||
|
backend/storage/framework/sessions/*
|
||||||
|
backend/storage/framework/views/*
|
||||||
|
backend/bootstrap/cache/*
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
tests/
|
||||||
|
backend/tests/
|
||||||
|
frontend/tests/
|
||||||
|
|
||||||
|
# Data directories
|
||||||
|
docker/data/
|
||||||
6
.env
Normal file
6
.env
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
APP_KEY=base64:dGVzdGluZ19rZXlfZm9yX3Byb2R1Y3Rpb25fdGVzdGluZ19vbmx5X25vdF9zZWN1cmU=
|
||||||
|
DB_PASSWORD=test_password_123
|
||||||
|
MYSQL_ROOT_PASSWORD=root_password_456
|
||||||
|
DB_USERNAME=trip_user
|
||||||
|
DB_DATABASE=trip_planner
|
||||||
|
APP_URL=http://localhost:8080
|
||||||
100
.woodpecker.yml
Normal file
100
.woodpecker.yml
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
# Woodpecker CI Pipeline for Trip Planner
|
||||||
|
# Builds and pushes production Docker image to Codeberg Container Registry
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- &image_repo 'codeberg.org/lvl0/trip-planner'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Extract version from commit message if merging from release branch
|
||||||
|
- name: extract-version
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
# Install git and grep with PCRE support
|
||||||
|
apk add --no-cache git grep
|
||||||
|
|
||||||
|
# Get the commit message to check if it's a merge from release branch
|
||||||
|
COMMIT_MSG=$(git log -1 --pretty=%B)
|
||||||
|
|
||||||
|
# Try to extract version from merge commit message (e.g., "Merge branch 'release/v0.1.0'")
|
||||||
|
VERSION=$(echo "$COMMIT_MSG" | grep -oP "release/v?\K[0-9]+\.[0-9]+\.[0-9]+" || echo "")
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
# No version found, use commit SHA as version
|
||||||
|
VERSION="dev-$(git rev-parse --short HEAD)"
|
||||||
|
echo "No release version detected, using: $VERSION"
|
||||||
|
echo "$VERSION" > /woodpecker/version.txt
|
||||||
|
else
|
||||||
|
echo "Detected release version: $VERSION"
|
||||||
|
echo "$VERSION" > /woodpecker/version.txt
|
||||||
|
# Export for use in build step
|
||||||
|
echo "export VERSION_TAG=$VERSION" >> /tmp/version_env.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build and push with latest tag (always)
|
||||||
|
- name: build-and-push-latest
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: *image_repo
|
||||||
|
registry: codeberg.org
|
||||||
|
username:
|
||||||
|
from_secret: container_registry_username
|
||||||
|
password:
|
||||||
|
from_secret: container_registry_token
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
build_args:
|
||||||
|
- BUILDKIT_INLINE_CACHE=1
|
||||||
|
auto_tag: false
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
depends_on:
|
||||||
|
- extract-version
|
||||||
|
|
||||||
|
# Check if this is a release build
|
||||||
|
- name: check-release
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
VERSION=$(cat /woodpecker/version.txt)
|
||||||
|
if echo "$VERSION" | grep -qv "^dev-"; then
|
||||||
|
echo "Release version detected: $VERSION"
|
||||||
|
echo "true" > /woodpecker/is_release.txt
|
||||||
|
else
|
||||||
|
echo "Development build ($VERSION), will skip version tag"
|
||||||
|
echo "false" > /woodpecker/is_release.txt
|
||||||
|
fi
|
||||||
|
depends_on:
|
||||||
|
- build-and-push-latest
|
||||||
|
|
||||||
|
# Notify build status
|
||||||
|
- name: notify-success
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
VERSION=$(cat /woodpecker/version.txt 2>/dev/null || echo "unknown")
|
||||||
|
IS_RELEASE=$(cat /woodpecker/is_release.txt 2>/dev/null || echo "false")
|
||||||
|
echo "✅ Successfully built and pushed trip-planner:latest"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
if [ "$IS_RELEASE" = "true" ]; then
|
||||||
|
echo "ℹ️ This is a release build. To tag with version $VERSION:"
|
||||||
|
echo " docker pull codeberg.org/lvl0/trip-planner:latest"
|
||||||
|
echo " docker tag codeberg.org/lvl0/trip-planner:latest codeberg.org/lvl0/trip-planner:$VERSION"
|
||||||
|
echo " docker push codeberg.org/lvl0/trip-planner:$VERSION"
|
||||||
|
fi
|
||||||
|
echo "Image: codeberg.org/lvl0/trip-planner:latest"
|
||||||
|
when:
|
||||||
|
status: success
|
||||||
|
|
||||||
|
- name: notify-failure
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- echo "❌ Build failed! Check the logs above for details."
|
||||||
|
when:
|
||||||
|
status: failure
|
||||||
134
Dockerfile.prod
Normal file
134
Dockerfile.prod
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 1: Build Frontend
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:20-alpine AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
|
# Copy frontend package files
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies (including dev for build)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy frontend source
|
||||||
|
COPY frontend/ ./
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 2: Build Backend Dependencies
|
||||||
|
# =============================================================================
|
||||||
|
FROM php:8.3-fpm-alpine AS backend-builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
libpng-dev \
|
||||||
|
oniguruma-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
zip \
|
||||||
|
unzip
|
||||||
|
|
||||||
|
# Install PHP extensions
|
||||||
|
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd opcache
|
||||||
|
|
||||||
|
# Install Composer
|
||||||
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
# Copy composer files
|
||||||
|
COPY backend/composer.json backend/composer.lock ./
|
||||||
|
|
||||||
|
# Install PHP dependencies (production only)
|
||||||
|
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
|
||||||
|
|
||||||
|
# Copy backend source
|
||||||
|
COPY backend/ ./
|
||||||
|
|
||||||
|
# Generate optimized autoloader
|
||||||
|
RUN composer dump-autoload --optimize --classmap-authoritative
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage 3: Final Production Image (All-in-One)
|
||||||
|
# =============================================================================
|
||||||
|
FROM php:8.3-fpm-alpine
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
libpng \
|
||||||
|
oniguruma \
|
||||||
|
libxml2 \
|
||||||
|
nginx \
|
||||||
|
supervisor \
|
||||||
|
mariadb \
|
||||||
|
mariadb-client \
|
||||||
|
redis \
|
||||||
|
curl \
|
||||||
|
bash
|
||||||
|
|
||||||
|
# Copy PHP extensions from backend-builder
|
||||||
|
COPY --from=backend-builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
|
||||||
|
COPY --from=backend-builder /usr/local/etc/php/conf.d/docker-php-ext-*.ini /usr/local/etc/php/conf.d/
|
||||||
|
|
||||||
|
# Configure PHP for production
|
||||||
|
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||||
|
echo "opcache.memory_consumption=128" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||||
|
echo "opcache.max_accelerated_files=10000" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||||
|
echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini && \
|
||||||
|
echo "expose_php=0" >> /usr/local/etc/php/conf.d/security.ini && \
|
||||||
|
echo "display_errors=0" >> /usr/local/etc/php/conf.d/security.ini && \
|
||||||
|
echo "log_errors=1" >> /usr/local/etc/php/conf.d/security.ini
|
||||||
|
|
||||||
|
# Create application user
|
||||||
|
RUN addgroup -g 1000 appuser && \
|
||||||
|
adduser -D -u 1000 -G appuser appuser && \
|
||||||
|
mkdir -p /var/www/html /usr/share/nginx/html && \
|
||||||
|
chown -R appuser:appuser /var/www/html /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Create necessary directories for services
|
||||||
|
RUN mkdir -p /run/nginx /run/mysqld /var/lib/mysql /var/log/supervisor /data/redis && \
|
||||||
|
chown -R appuser:appuser /run/nginx /usr/share/nginx/html && \
|
||||||
|
chown -R mysql:mysql /run/mysqld /var/lib/mysql && \
|
||||||
|
chown -R redis:redis /data/redis
|
||||||
|
|
||||||
|
# Copy backend from builder
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
COPY --from=backend-builder --chown=appuser:appuser /var/www/html ./
|
||||||
|
|
||||||
|
# Create Laravel required directories
|
||||||
|
RUN mkdir -p storage/framework/{sessions,views,cache} \
|
||||||
|
storage/logs \
|
||||||
|
bootstrap/cache && \
|
||||||
|
chown -R appuser:appuser storage bootstrap/cache && \
|
||||||
|
chmod -R 775 storage bootstrap/cache
|
||||||
|
|
||||||
|
# Copy frontend built assets
|
||||||
|
COPY --from=frontend-builder --chown=appuser:appuser /app/frontend/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configurations
|
||||||
|
COPY docker/nginx/production.conf /etc/nginx/http.d/default.conf
|
||||||
|
|
||||||
|
# Copy supervisor configuration
|
||||||
|
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
# Initialize MariaDB data directory
|
||||||
|
RUN mysql_install_db --user=mysql --datadir=/var/lib/mysql
|
||||||
|
|
||||||
|
# Copy entrypoint script
|
||||||
|
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/up || exit 1
|
||||||
|
|
||||||
|
# Expose HTTP port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Set entrypoint
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|
||||||
|
# Start supervisor
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
48
backend/.dockerignore
Normal file
48
backend/.dockerignore
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
tests/
|
||||||
|
.phpunit.result.cache
|
||||||
|
phpunit.xml
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
.editorconfig
|
||||||
|
.php-cs-fixer.php
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
phpstan.neon
|
||||||
|
phpstan-baseline.neon
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Storage and cache (will be generated in container)
|
||||||
|
storage/framework/cache/*
|
||||||
|
storage/framework/sessions/*
|
||||||
|
storage/framework/views/*
|
||||||
|
storage/logs/*
|
||||||
|
bootstrap/cache/*
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yml
|
||||||
|
.dockerignore
|
||||||
|
|
@ -1,58 +1,66 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
app:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: .
|
||||||
dockerfile: ../docker/frontend/Dockerfile.prod
|
dockerfile: Dockerfile.prod
|
||||||
container_name: trip-planner-frontend
|
container_name: trip-planner-production
|
||||||
ports:
|
ports:
|
||||||
- "${FRONTEND_PORT:-80}:80"
|
- "${APP_PORT:-8080}:80"
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- trip-planner-network
|
|
||||||
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: ../docker/backend/Dockerfile.prod
|
|
||||||
container_name: trip-planner-backend
|
|
||||||
ports:
|
|
||||||
- "${BACKEND_PORT:-8080}:80"
|
|
||||||
environment:
|
environment:
|
||||||
|
# Laravel Application
|
||||||
|
APP_NAME: "Trip Planner"
|
||||||
APP_ENV: production
|
APP_ENV: production
|
||||||
APP_DEBUG: false
|
APP_DEBUG: false
|
||||||
APP_URL: ${APP_URL}
|
APP_KEY: ${APP_KEY}
|
||||||
|
APP_URL: ${APP_URL:-http://localhost:8080}
|
||||||
|
|
||||||
|
# Database (internal MariaDB)
|
||||||
DB_CONNECTION: mysql
|
DB_CONNECTION: mysql
|
||||||
DB_HOST: ${DB_HOST}
|
DB_HOST: 127.0.0.1
|
||||||
DB_PORT: ${DB_PORT:-3306}
|
DB_PORT: 3306
|
||||||
DB_DATABASE: ${DB_DATABASE}
|
DB_DATABASE: ${DB_DATABASE:-trip_planner}
|
||||||
DB_USERNAME: ${DB_USERNAME}
|
DB_USERNAME: ${DB_USERNAME:-trip_user}
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD must be set}
|
||||||
REDIS_HOST: redis
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD must be set}
|
||||||
|
|
||||||
|
# Redis (internal)
|
||||||
|
REDIS_HOST: 127.0.0.1
|
||||||
REDIS_PORT: 6379
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: null
|
||||||
|
|
||||||
|
# Cache & Session
|
||||||
CACHE_DRIVER: redis
|
CACHE_DRIVER: redis
|
||||||
QUEUE_CONNECTION: redis
|
QUEUE_CONNECTION: redis
|
||||||
SESSION_DRIVER: redis
|
SESSION_DRIVER: redis
|
||||||
depends_on:
|
SESSION_LIFETIME: 120
|
||||||
- redis
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- trip-planner-network
|
|
||||||
|
|
||||||
redis:
|
# Mail (configure as needed)
|
||||||
image: docker.io/library/redis:alpine
|
MAIL_MAILER: ${MAIL_MAILER:-log}
|
||||||
container_name: trip-planner-redis
|
MAIL_HOST: ${MAIL_HOST:-}
|
||||||
|
MAIL_PORT: ${MAIL_PORT:-}
|
||||||
|
MAIL_USERNAME: ${MAIL_USERNAME:-}
|
||||||
|
MAIL_PASSWORD: ${MAIL_PASSWORD:-}
|
||||||
|
MAIL_ENCRYPTION: ${MAIL_ENCRYPTION:-}
|
||||||
|
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS:-noreply@tripplanner.local}
|
||||||
|
MAIL_FROM_NAME: "${MAIL_FROM_NAME:-Trip Planner}"
|
||||||
volumes:
|
volumes:
|
||||||
- redis-data:/data
|
# Persistent data for database
|
||||||
command: redis-server --appendonly yes
|
- db-data:/var/lib/mysql
|
||||||
|
# Persistent data for redis
|
||||||
|
- redis-data:/data/redis
|
||||||
|
# Persistent storage for uploaded files
|
||||||
|
- storage-data:/var/www/html/storage/app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
healthcheck:
|
||||||
- trip-planner-network
|
test: ["CMD", "curl", "-f", "http://localhost/up"]
|
||||||
|
interval: 30s
|
||||||
networks:
|
timeout: 10s
|
||||||
trip-planner-network:
|
retries: 3
|
||||||
driver: bridge
|
start_period: 60s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis-data:
|
db-data:
|
||||||
|
redis-data:
|
||||||
|
storage-data:
|
||||||
160
docker/README.md
Normal file
160
docker/README.md
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Trip Planner - Docker Setup
|
||||||
|
|
||||||
|
This directory contains Docker configurations for both development and production environments.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/
|
||||||
|
├── README.md # This file
|
||||||
|
├── dev/ # Development environment documentation
|
||||||
|
│ └── README.md
|
||||||
|
├── prod/ # Production environment documentation
|
||||||
|
│ └── README.md
|
||||||
|
├── backend/ # Backend service configurations
|
||||||
|
│ ├── Dockerfile.dev
|
||||||
|
│ ├── Dockerfile.prod
|
||||||
|
│ └── supervisord.conf
|
||||||
|
├── frontend/ # Frontend service configurations
|
||||||
|
│ ├── Dockerfile.dev
|
||||||
|
│ └── Dockerfile.prod
|
||||||
|
├── nginx/ # Nginx configurations
|
||||||
|
│ ├── backend.conf # Backend API nginx config
|
||||||
|
│ ├── frontend.conf # Frontend SPA nginx config
|
||||||
|
│ └── production.conf # All-in-one production nginx config
|
||||||
|
├── supervisord.conf # Supervisord config for production
|
||||||
|
└── entrypoint.sh # Production container entrypoint script
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environments
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
|
||||||
|
**Architecture**: Multi-container setup with separate services
|
||||||
|
- Frontend (React + Vite dev server)
|
||||||
|
- Backend (Laravel + PHP-FPM)
|
||||||
|
- Database (MariaDB)
|
||||||
|
- Redis
|
||||||
|
- Mailpit (email testing)
|
||||||
|
|
||||||
|
**Use case**: Local development with hot-reloading and debugging
|
||||||
|
|
||||||
|
**Documentation**: See [dev/README.md](dev/README.md)
|
||||||
|
|
||||||
|
**Docker Compose**: `docker-compose.dev.yml` (in project root)
|
||||||
|
|
||||||
|
### Production Environment
|
||||||
|
|
||||||
|
**Architecture**: Single all-in-one container with all services
|
||||||
|
- Frontend (built React app served by Nginx)
|
||||||
|
- Backend (Laravel + PHP-FPM + Nginx)
|
||||||
|
- Database (MariaDB - internal)
|
||||||
|
- Redis (internal)
|
||||||
|
- All managed by Supervisord
|
||||||
|
|
||||||
|
**Use case**: Production deployment with minimal footprint
|
||||||
|
|
||||||
|
**Documentation**: See [prod/README.md](prod/README.md)
|
||||||
|
|
||||||
|
**Docker Compose**: `docker-compose.prod.yml` (in project root)
|
||||||
|
|
||||||
|
## Key Differences
|
||||||
|
|
||||||
|
| Aspect | Development | Production |
|
||||||
|
|--------|------------|------------|
|
||||||
|
| **Containers** | Multiple (5 services) | Single all-in-one |
|
||||||
|
| **Frontend** | Vite dev server with HMR | Pre-built static files |
|
||||||
|
| **Backend** | Live code mounting | Copied into image |
|
||||||
|
| **Database** | Separate container | Internal to main container |
|
||||||
|
| **Redis** | Separate container | Internal to main container |
|
||||||
|
| **Volumes** | Source code mounted | Persistent data only |
|
||||||
|
| **Ports** | Multiple (5173, 8000, 3306, etc.) | Single port (80) |
|
||||||
|
| **Size** | ~2GB+ | ~800MB |
|
||||||
|
|
||||||
|
## Port Allocation
|
||||||
|
|
||||||
|
### Development (default ports)
|
||||||
|
- Frontend: 5173
|
||||||
|
- Backend: 8000
|
||||||
|
- Database: 3306
|
||||||
|
- Redis: 6379
|
||||||
|
- Mailpit UI: 8025
|
||||||
|
- Mailpit SMTP: 1025
|
||||||
|
|
||||||
|
### Production (default ports)
|
||||||
|
- Application: 8080 (configurable via `APP_PORT`)
|
||||||
|
|
||||||
|
**Note**: When running both dev and production locally, ensure they don't use conflicting ports. The production setup defaults to port 8080 to avoid conflicts with the dev setup.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
# Start all dev services
|
||||||
|
docker compose -f docker-compose.dev.yml up
|
||||||
|
|
||||||
|
# Stop all dev services
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production (Local Testing)
|
||||||
|
```bash
|
||||||
|
# Build and start production container
|
||||||
|
docker compose -f docker-compose.prod.yml up --build
|
||||||
|
|
||||||
|
# Stop production container
|
||||||
|
docker compose -f docker-compose.prod.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Both environments use environment variables for configuration:
|
||||||
|
|
||||||
|
- **Development**: `.env.local` in project root
|
||||||
|
- **Production**: `.env` or pass via docker-compose environment section
|
||||||
|
|
||||||
|
See the respective README files for detailed environment variable documentation.
|
||||||
|
|
||||||
|
## Building Images
|
||||||
|
|
||||||
|
### Development
|
||||||
|
Development images are built automatically when you run `docker compose up`.
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```bash
|
||||||
|
# Build the production image
|
||||||
|
docker build -f Dockerfile.prod -t trip-planner:latest .
|
||||||
|
|
||||||
|
# Or use docker-compose
|
||||||
|
docker compose -f docker-compose.prod.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
The production image is automatically built and pushed to Codeberg Container Registry when changes are merged to the `main` branch.
|
||||||
|
|
||||||
|
See `.woodpecker.yml` in the project root for pipeline configuration.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Development Issues
|
||||||
|
See [dev/README.md](dev/README.md#troubleshooting)
|
||||||
|
|
||||||
|
### Production Issues
|
||||||
|
See [prod/README.md](prod/README.md#troubleshooting)
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Development setup runs with elevated privileges for convenience
|
||||||
|
- Production setup follows security best practices:
|
||||||
|
- Non-root users where possible
|
||||||
|
- Minimal base images
|
||||||
|
- No unnecessary privileges
|
||||||
|
- Security headers configured
|
||||||
|
- Internal services (DB, Redis) bound to localhost only
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- Check the specific environment README files in `dev/` or `prod/`
|
||||||
|
- Review the main project documentation
|
||||||
|
- Check container logs: `docker logs <container-name>`
|
||||||
342
docker/dev/README.md
Normal file
342
docker/dev/README.md
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
# Trip Planner - Development Environment
|
||||||
|
|
||||||
|
This document describes the development Docker setup for Trip Planner.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The development environment uses a multi-container architecture with Docker Compose, providing:
|
||||||
|
- **Hot Module Replacement (HMR)** for frontend development
|
||||||
|
- **Live code mounting** for instant backend changes
|
||||||
|
- **Separate services** for easy debugging
|
||||||
|
- **Development tools** like Mailpit for email testing
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ trip-planner-network (Docker Bridge Network) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
|
||||||
|
│ │ Frontend │ │ Backend │ │ Database │ │
|
||||||
|
│ │ React+Vite │ │ Laravel+PHP │ │ MariaDB │ │
|
||||||
|
│ │ Port: 5173 │ │ Port: 8000 │ │ Port:3306│ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Redis │ │ Mailpit │ │
|
||||||
|
│ │ Port: 6379 │ │ UI: 8025 │ │
|
||||||
|
│ └──────────────┘ │ SMTP: 1025 │ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
### Frontend (trip-planner-frontend-dev)
|
||||||
|
- **Image**: Built from `docker/frontend/Dockerfile.dev`
|
||||||
|
- **Port**: 5173
|
||||||
|
- **Technology**: React 19 + Vite 7
|
||||||
|
- **Features**: Hot Module Replacement, ESLint
|
||||||
|
- **Volume**: `./frontend:/app` (live code mounting)
|
||||||
|
|
||||||
|
### Backend (trip-planner-backend-dev)
|
||||||
|
- **Image**: Built from `docker/backend/Dockerfile.dev`
|
||||||
|
- **Port**: 8000
|
||||||
|
- **Technology**: Laravel 12 + PHP 8.3
|
||||||
|
- **Features**: Artisan commands, PHP-FPM
|
||||||
|
- **Volume**: `./backend:/var/www/html` (live code mounting)
|
||||||
|
|
||||||
|
### Database (trip-planner-db-dev)
|
||||||
|
- **Image**: MariaDB 11
|
||||||
|
- **Port**: 3306
|
||||||
|
- **Data**: Persisted in `./docker/data/mysql-data`
|
||||||
|
- **Credentials**: Configured via `.env.local`
|
||||||
|
|
||||||
|
### Redis (trip-planner-redis-dev)
|
||||||
|
- **Image**: Redis Alpine
|
||||||
|
- **Port**: 6379
|
||||||
|
- **Usage**: Cache, sessions, queues
|
||||||
|
- **Data**: Named volume `redis-data`
|
||||||
|
|
||||||
|
### Mailpit (trip-planner-mailpit-dev)
|
||||||
|
- **Image**: Axllent Mailpit
|
||||||
|
- **Ports**:
|
||||||
|
- SMTP: 1025
|
||||||
|
- Web UI: 8025
|
||||||
|
- **Usage**: Email testing (catches all outgoing emails)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker Engine 20.10+
|
||||||
|
- Docker Compose 2.0+
|
||||||
|
- At least 4GB RAM available for Docker
|
||||||
|
|
||||||
|
### Initial Setup
|
||||||
|
|
||||||
|
1. **Clone the repository** (if not already done):
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@codeberg.org/lvl0/trip-planner.git
|
||||||
|
cd trip-planner
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create environment file**:
|
||||||
|
```bash
|
||||||
|
# Copy the example environment file
|
||||||
|
cp .env.local.example .env.local
|
||||||
|
|
||||||
|
# Edit .env.local with your settings
|
||||||
|
nano .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the development environment**:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Wait for services to be ready** (check with):
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Run initial Laravel setup**:
|
||||||
|
```bash
|
||||||
|
# Generate application key
|
||||||
|
docker exec trip-planner-backend-dev php artisan key:generate
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
docker exec trip-planner-backend-dev php artisan migrate
|
||||||
|
|
||||||
|
# Seed database (optional)
|
||||||
|
docker exec trip-planner-backend-dev php artisan db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Access the application**:
|
||||||
|
- Frontend: http://localhost:5173
|
||||||
|
- Backend API: http://localhost:8000
|
||||||
|
- Mailpit UI: http://localhost:8025
|
||||||
|
|
||||||
|
### Daily Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start all services
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose -f docker-compose.dev.yml logs -f
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# Restart a specific service
|
||||||
|
docker compose -f docker-compose.dev.yml restart backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The development environment reads from `.env.local` in the project root.
|
||||||
|
|
||||||
|
### Required Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Application
|
||||||
|
APP_NAME="Trip Planner"
|
||||||
|
APP_KEY=base64:your-generated-key-here
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_DATABASE=trip_planner
|
||||||
|
DB_USERNAME=trip_user
|
||||||
|
DB_PASSWORD=secret
|
||||||
|
|
||||||
|
# Mail
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=mailpit
|
||||||
|
MAIL_PORT=1025
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Frontend
|
||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Backend Tasks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run Artisan commands
|
||||||
|
docker exec trip-planner-backend-dev php artisan <command>
|
||||||
|
|
||||||
|
# Examples:
|
||||||
|
docker exec trip-planner-backend-dev php artisan migrate
|
||||||
|
docker exec trip-planner-backend-dev php artisan make:controller UserController
|
||||||
|
docker exec trip-planner-backend-dev php artisan tinker
|
||||||
|
|
||||||
|
# Install PHP dependencies
|
||||||
|
docker exec trip-planner-backend-dev composer install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
docker exec trip-planner-backend-dev php artisan test
|
||||||
|
|
||||||
|
# Clear caches
|
||||||
|
docker exec trip-planner-backend-dev php artisan cache:clear
|
||||||
|
docker exec trip-planner-backend-dev php artisan config:clear
|
||||||
|
docker exec trip-planner-backend-dev php artisan route:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Tasks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install npm dependencies
|
||||||
|
docker exec trip-planner-frontend-dev npm install
|
||||||
|
|
||||||
|
# Run linter
|
||||||
|
docker exec trip-planner-frontend-dev npm run lint
|
||||||
|
|
||||||
|
# Build for preview
|
||||||
|
docker exec trip-planner-frontend-dev npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Tasks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access MySQL shell
|
||||||
|
docker exec -it trip-planner-db-dev mysql -u trip_user -p trip_planner
|
||||||
|
|
||||||
|
# Backup database
|
||||||
|
docker exec trip-planner-db-dev mysqldump -u trip_user -p trip_planner > backup.sql
|
||||||
|
|
||||||
|
# Restore database
|
||||||
|
docker exec -i trip-planner-db-dev mysql -u trip_user -p trip_planner < backup.sql
|
||||||
|
|
||||||
|
# Reset database
|
||||||
|
docker compose -f docker-compose.dev.yml down -v # Removes volumes!
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
docker exec trip-planner-backend-dev php artisan migrate --seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewing Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker compose -f docker-compose.dev.yml logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker compose -f docker-compose.dev.yml logs -f backend
|
||||||
|
|
||||||
|
# Laravel logs
|
||||||
|
docker exec trip-planner-backend-dev tail -f storage/logs/laravel.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Services won't start
|
||||||
|
|
||||||
|
**Check for port conflicts:**
|
||||||
|
```bash
|
||||||
|
# Check what's using the ports
|
||||||
|
lsof -i :5173 # Frontend
|
||||||
|
lsof -i :8000 # Backend
|
||||||
|
lsof -i :3306 # Database
|
||||||
|
|
||||||
|
# Stop conflicting services or change ports in docker-compose.dev.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend HMR not working
|
||||||
|
|
||||||
|
**SELinux issue (Fedora/RHEL):**
|
||||||
|
The `:Z` flag in volume mounts handles this, but if HMR still doesn't work:
|
||||||
|
```bash
|
||||||
|
# Check if SELinux is enforcing
|
||||||
|
getenforce
|
||||||
|
|
||||||
|
# If needed, you can temporarily set to permissive
|
||||||
|
sudo setenforce 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend not connecting to database
|
||||||
|
|
||||||
|
**Wait for database to be fully ready:**
|
||||||
|
```bash
|
||||||
|
# Check database status
|
||||||
|
docker compose -f docker-compose.dev.yml ps database
|
||||||
|
|
||||||
|
# Check database logs
|
||||||
|
docker compose -f docker-compose.dev.yml logs database
|
||||||
|
|
||||||
|
# Verify connection
|
||||||
|
docker exec trip-planner-backend-dev php artisan migrate:status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission issues
|
||||||
|
|
||||||
|
**Vendor/node_modules ownership:**
|
||||||
|
```bash
|
||||||
|
# Fix backend vendor permissions
|
||||||
|
docker exec trip-planner-backend-dev chown -R www-data:www-data vendor
|
||||||
|
|
||||||
|
# Fix frontend node_modules (usually not needed with named volumes)
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
docker volume rm trip-planner_node_modules
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean slate rebuild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop everything
|
||||||
|
docker compose -f docker-compose.dev.yml down -v
|
||||||
|
|
||||||
|
# Remove images
|
||||||
|
docker rmi trip-planner-frontend-dev trip-planner-backend-dev
|
||||||
|
|
||||||
|
# Rebuild and start
|
||||||
|
docker compose -f docker-compose.dev.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
### Disable Features You Don't Need
|
||||||
|
|
||||||
|
If a service is not needed for your current task:
|
||||||
|
```bash
|
||||||
|
# Start only specific services
|
||||||
|
docker compose -f docker-compose.dev.yml up -d backend database redis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Cached Volumes
|
||||||
|
|
||||||
|
The dev setup uses named volumes for `node_modules` and `vendor` to improve performance:
|
||||||
|
- `node_modules`: Frontend dependencies
|
||||||
|
- `vendor`: Backend PHP dependencies
|
||||||
|
|
||||||
|
These are NOT mounted from your host, keeping filesystem operations fast.
|
||||||
|
|
||||||
|
## Differences from Production
|
||||||
|
|
||||||
|
| Feature | Development | Production |
|
||||||
|
|---------|------------|------------|
|
||||||
|
| Code loading | Live mounted volumes | Copied into image |
|
||||||
|
| Caching | Disabled/minimal | Aggressive (OPcache, etc.) |
|
||||||
|
| Error display | Verbose | Hidden |
|
||||||
|
| Debug mode | Enabled | Disabled |
|
||||||
|
| Privileges | Elevated for convenience | Minimal (security) |
|
||||||
|
| Rebuilding | Rarely needed | Required for changes |
|
||||||
|
|
||||||
|
## Security Note
|
||||||
|
|
||||||
|
⚠️ **The development environment is NOT secure** - it runs with `privileged: true` for convenience and mounts source code directly. **Never use this setup in production!**
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- Check the main [docker/README.md](../README.md)
|
||||||
|
- Review Laravel logs: `docker exec trip-planner-backend-dev tail -f storage/logs/laravel.log`
|
||||||
|
- Check container health: `docker compose -f docker-compose.dev.yml ps`
|
||||||
|
- Inspect a container: `docker inspect trip-planner-backend-dev`
|
||||||
141
docker/entrypoint.sh
Normal file
141
docker/entrypoint.sh
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
MYSQL_WAIT_ATTEMPTS=15
|
||||||
|
MYSQL_WAIT_INTERVAL=2
|
||||||
|
REDIS_WAIT_ATTEMPTS=10
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo "[INIT] Trip Planner - Production Container Init"
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# Validate required environment variables
|
||||||
|
if [ -z "$APP_KEY" ]; then
|
||||||
|
echo "[INIT] ERROR: APP_KEY is not set!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Laravel APP_KEY must be base64: prefix + minimum 44 chars (32 bytes base64-encoded)
|
||||||
|
if [[ ! "$APP_KEY" =~ ^base64:.{44,}$ ]]; then
|
||||||
|
echo "[INIT] ERROR: APP_KEY format is invalid! Must be base64:xxxxx (generated with 'php artisan key:generate')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$DB_PASSWORD" ]; then
|
||||||
|
echo "[INIT] ERROR: DB_PASSWORD is not set!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
|
||||||
|
echo "[INIT] ERROR: MYSQL_ROOT_PASSWORD is not set!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start MariaDB in background
|
||||||
|
echo "[INIT] Starting MariaDB..."
|
||||||
|
mysqld --user=mysql --datadir=/var/lib/mysql --bind-address=127.0.0.1 &
|
||||||
|
MYSQL_PID=$!
|
||||||
|
|
||||||
|
# Start Redis in background
|
||||||
|
echo "[INIT] Starting Redis..."
|
||||||
|
redis-server --bind 127.0.0.1 --port 6379 --dir /data/redis --appendonly yes --daemonize yes
|
||||||
|
|
||||||
|
# Wait for MariaDB to be ready
|
||||||
|
echo "[INIT] Waiting for MariaDB to be ready..."
|
||||||
|
for i in $(seq 1 $MYSQL_WAIT_ATTEMPTS); do
|
||||||
|
if mysqladmin ping --socket=/run/mysqld/mysqld.sock --silent 2>/dev/null; then
|
||||||
|
echo "[INIT] MariaDB is up! (took ${i} attempts)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -eq $MYSQL_WAIT_ATTEMPTS ]; then
|
||||||
|
echo "[INIT] ERROR: MariaDB failed to start within $((MYSQL_WAIT_ATTEMPTS * MYSQL_WAIT_INTERVAL)) seconds"
|
||||||
|
kill $MYSQL_PID 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep $MYSQL_WAIT_INTERVAL
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for Redis to be ready
|
||||||
|
echo "[INIT] Waiting for Redis to be ready..."
|
||||||
|
for i in $(seq 1 $REDIS_WAIT_ATTEMPTS); do
|
||||||
|
if redis-cli ping 2>/dev/null | grep -q PONG; then
|
||||||
|
echo "[INIT] Redis is up! (took ${i} attempts)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -eq $REDIS_WAIT_ATTEMPTS ]; then
|
||||||
|
echo "[INIT] ERROR: Redis failed to start within ${REDIS_WAIT_ATTEMPTS} seconds"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if database needs initialization
|
||||||
|
echo "[INIT] Checking database initialization..."
|
||||||
|
if ! mysql --socket=/run/mysqld/mysqld.sock -u root -p"${MYSQL_ROOT_PASSWORD}" -e "SELECT 1" &>/dev/null; then
|
||||||
|
echo "[INIT] Setting root password for first-time setup..."
|
||||||
|
mysqladmin --socket=/run/mysqld/mysqld.sock -u root password "${MYSQL_ROOT_PASSWORD}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create database and user if they don't exist
|
||||||
|
echo "[INIT] Ensuring database and user exist..."
|
||||||
|
# Escape single quotes in password for SQL safety
|
||||||
|
DB_PASSWORD_ESCAPED=$(echo "${DB_PASSWORD}" | sed "s/'/''/g")
|
||||||
|
mysql --socket=/run/mysqld/mysqld.sock -u root -p"${MYSQL_ROOT_PASSWORD}" <<EOSQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS \`${DB_DATABASE}\`;
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'localhost' IDENTIFIED BY '${DB_PASSWORD_ESCAPED}';
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON \`${DB_DATABASE}\`.* TO '${DB_USERNAME}'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
EOSQL
|
||||||
|
echo "[INIT] Database setup verified"
|
||||||
|
|
||||||
|
# Fix permissions for persistent volumes
|
||||||
|
echo "[INIT] Fixing permissions..."
|
||||||
|
chown -R redis:redis /data/redis 2>/dev/null || true
|
||||||
|
chown -R appuser:appuser /var/www/html/storage 2>/dev/null || true
|
||||||
|
chmod -R 775 /var/www/html/storage 2>/dev/null || true
|
||||||
|
|
||||||
|
# Run Laravel migrations
|
||||||
|
echo "[INIT] Running Laravel migrations..."
|
||||||
|
cd /var/www/html
|
||||||
|
php artisan migrate --force
|
||||||
|
|
||||||
|
# Ensure storage link exists
|
||||||
|
if [ ! -L /var/www/html/public/storage ]; then
|
||||||
|
echo "[INIT] Creating storage link..."
|
||||||
|
php artisan storage:link
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop background services gracefully (supervisor will manage them)
|
||||||
|
echo "[INIT] Stopping temporary database services..."
|
||||||
|
|
||||||
|
# Stop Redis first (faster shutdown)
|
||||||
|
echo "[INIT] Stopping temporary Redis..."
|
||||||
|
redis-cli shutdown 2>/dev/null || true
|
||||||
|
|
||||||
|
# Gracefully stop MariaDB
|
||||||
|
echo "[INIT] Stopping temporary MariaDB..."
|
||||||
|
mysqladmin --socket=/run/mysqld/mysqld.sock -u root -p"${MYSQL_ROOT_PASSWORD}" shutdown 2>/dev/null || kill $MYSQL_PID 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait for processes to actually terminate
|
||||||
|
echo "[INIT] Waiting for services to fully stop..."
|
||||||
|
for i in {1..15}; do
|
||||||
|
if ! pgrep -x mysqld >/dev/null && ! pgrep -x redis-server >/dev/null; then
|
||||||
|
echo "[INIT] All temporary services stopped successfully"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -eq 15 ]; then
|
||||||
|
echo "[INIT] WARNING: Services took too long to stop, forcing..."
|
||||||
|
pkill -9 mysqld || true
|
||||||
|
pkill -9 redis-server || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo "[INIT] Initialization complete! Starting services..."
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# Execute the main command (supervisord)
|
||||||
|
exec "$@"
|
||||||
100
docker/nginx/production.conf
Normal file
100
docker/nginx/production.conf
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Disable server tokens
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml application/atom+xml image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype;
|
||||||
|
|
||||||
|
# API Backend
|
||||||
|
location /api {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_buffer_size 32k;
|
||||||
|
fastcgi_buffers 8 16k;
|
||||||
|
fastcgi_read_timeout 240;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sanctum/CSRF cookie endpoint
|
||||||
|
location /sanctum/csrf-cookie {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Storage files (user uploads, etc)
|
||||||
|
location /storage {
|
||||||
|
alias /var/www/html/storage/app/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Frontend SPA
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoints
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Laravel health check endpoint
|
||||||
|
location /up {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to hidden files
|
||||||
|
location ~ /\.(?!well-known).* {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
}
|
||||||
559
docker/prod/README.md
Normal file
559
docker/prod/README.md
Normal file
|
|
@ -0,0 +1,559 @@
|
||||||
|
# Trip Planner - Production Environment
|
||||||
|
|
||||||
|
This document describes the production Docker setup for Trip Planner.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The production environment uses a **single all-in-one container** that includes:
|
||||||
|
- ✅ Frontend (React SPA built and served by Nginx)
|
||||||
|
- ✅ Backend (Laravel API with PHP-FPM)
|
||||||
|
- ✅ Database (MariaDB)
|
||||||
|
- ✅ Cache/Sessions (Redis)
|
||||||
|
- ✅ Web Server (Nginx as reverse proxy)
|
||||||
|
|
||||||
|
All services are managed by **Supervisord** within a single container.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ trip-planner-production (Single Container) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────┐ │
|
||||||
|
│ │ Nginx (Port 80) │ │
|
||||||
|
│ │ ┌──────────────┐ ┌──────────────┐ │ │
|
||||||
|
│ │ │ Frontend │ │ Backend API │ │ │
|
||||||
|
│ │ │ Static Files│ │ PHP-FPM:9000│ │ │
|
||||||
|
│ │ │ (/) │ │ (/api/*) │ │ │
|
||||||
|
│ │ └──────────────┘ └──────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ MariaDB │ │ Redis │ │
|
||||||
|
│ │ localhost │ │ localhost │ │
|
||||||
|
│ │ :3306 │ │ :6379 │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Managed by Supervisord │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
└─ Port 80 (or configured APP_PORT)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- ✅ Non-root users for services where possible
|
||||||
|
- ✅ Minimal Alpine-based image
|
||||||
|
- ✅ Database and Redis bound to localhost only
|
||||||
|
- ✅ Security headers configured
|
||||||
|
- ✅ OPcache enabled with production settings
|
||||||
|
- ✅ PHP display_errors disabled
|
||||||
|
|
||||||
|
### Optimization
|
||||||
|
- ✅ Multi-stage build (smaller image size ~800MB)
|
||||||
|
- ✅ OPcache with no timestamp validation
|
||||||
|
- ✅ Gzip compression enabled
|
||||||
|
- ✅ Static asset caching (1 year)
|
||||||
|
- ✅ Optimized Composer autoloader
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- ✅ Health checks configured
|
||||||
|
- ✅ Automatic service restart via Supervisord
|
||||||
|
- ✅ Persistent data volumes for database, redis, and storage
|
||||||
|
- ✅ Proper initialization and migration on startup
|
||||||
|
|
||||||
|
## Building the Image
|
||||||
|
|
||||||
|
### Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root
|
||||||
|
docker build -f Dockerfile.prod -t trip-planner:latest .
|
||||||
|
|
||||||
|
# Check image size
|
||||||
|
docker images trip-planner:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.prod.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD (Automatic)
|
||||||
|
|
||||||
|
The image is automatically built and pushed to Codeberg Container Registry when:
|
||||||
|
- Changes are merged to `main` branch
|
||||||
|
- Pipeline extracts version from merge commit (e.g., from `release/v0.1.0`)
|
||||||
|
- Tagged as both `latest` and version number (e.g., `0.1.0`)
|
||||||
|
|
||||||
|
## Running the Container
|
||||||
|
|
||||||
|
### Using Docker Compose (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the container
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose -f docker-compose.prod.yml logs -f
|
||||||
|
|
||||||
|
# Stop the container
|
||||||
|
docker compose -f docker-compose.prod.yml down
|
||||||
|
|
||||||
|
# Stop and remove volumes (⚠️ deletes data!)
|
||||||
|
docker compose -f docker-compose.prod.yml down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name trip-planner \
|
||||||
|
-p 8080:80 \
|
||||||
|
-e APP_KEY=base64:your-key-here \
|
||||||
|
-e DB_PASSWORD=secure-password \
|
||||||
|
-e DB_USERNAME=trip_user \
|
||||||
|
-e DB_DATABASE=trip_planner \
|
||||||
|
-v trip-planner-db:/var/lib/mysql \
|
||||||
|
-v trip-planner-redis:/data/redis \
|
||||||
|
-v trip-planner-storage:/var/www/html/storage/app \
|
||||||
|
trip-planner:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the Published Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull from Codeberg Container Registry
|
||||||
|
docker pull codeberg.org/lvl0/trip-planner:latest
|
||||||
|
|
||||||
|
# Or a specific version
|
||||||
|
docker pull codeberg.org/lvl0/trip-planner:0.1.0
|
||||||
|
|
||||||
|
# Run it
|
||||||
|
docker run -d \
|
||||||
|
--name trip-planner \
|
||||||
|
-p 8080:80 \
|
||||||
|
-e APP_KEY=base64:your-key-here \
|
||||||
|
-e DB_PASSWORD=secure-password \
|
||||||
|
codeberg.org/lvl0/trip-planner:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Required Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Application Key (generate with: php artisan key:generate)
|
||||||
|
APP_KEY=base64:your-generated-key-here
|
||||||
|
|
||||||
|
# Database Credentials
|
||||||
|
DB_PASSWORD=your-secure-password
|
||||||
|
DB_USERNAME=trip_user
|
||||||
|
DB_DATABASE=trip_planner
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional Variables (with defaults)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Application
|
||||||
|
APP_NAME="Trip Planner"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# Database (internal MariaDB)
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
|
||||||
|
# Redis (internal)
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
|
||||||
|
# Cache & Session
|
||||||
|
CACHE_DRIVER=redis
|
||||||
|
QUEUE_CONNECTION=redis
|
||||||
|
SESSION_DRIVER=redis
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
|
||||||
|
# Mail
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_FROM_ADDRESS=noreply@tripplanner.local
|
||||||
|
MAIL_FROM_NAME="Trip Planner"
|
||||||
|
|
||||||
|
# Ports
|
||||||
|
APP_PORT=8080 # External port to expose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Environment Variables
|
||||||
|
|
||||||
|
**Docker Compose (recommended):**
|
||||||
|
```yaml
|
||||||
|
# Create .env file in project root
|
||||||
|
APP_KEY=base64:...
|
||||||
|
DB_PASSWORD=secret
|
||||||
|
APP_URL=https://tripplanner.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker Run:**
|
||||||
|
```bash
|
||||||
|
docker run -e APP_KEY=base64:... -e DB_PASSWORD=secret ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persistent Data
|
||||||
|
|
||||||
|
The production setup uses three volumes for persistent data:
|
||||||
|
|
||||||
|
| Volume | Purpose | Path in Container |
|
||||||
|
|--------|---------|-------------------|
|
||||||
|
| `db-data` | MariaDB database files | `/var/lib/mysql` |
|
||||||
|
| `redis-data` | Redis persistence | `/data/redis` |
|
||||||
|
| `storage-data` | User uploads, files | `/var/www/html/storage/app` |
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup database
|
||||||
|
docker exec trip-planner-production mysqldump -u trip_user -p trip_planner > backup.sql
|
||||||
|
|
||||||
|
# Backup volumes
|
||||||
|
docker run --rm \
|
||||||
|
-v trip-planner-db-data:/data \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
alpine tar czf /backup/db-backup.tar.gz /data
|
||||||
|
|
||||||
|
# Backup uploaded files
|
||||||
|
docker run --rm \
|
||||||
|
-v trip-planner-storage-data:/data \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
alpine tar czf /backup/storage-backup.tar.gz /data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restore database
|
||||||
|
docker exec -i trip-planner-production mysql -u trip_user -p trip_planner < backup.sql
|
||||||
|
|
||||||
|
# Restore volumes
|
||||||
|
docker run --rm \
|
||||||
|
-v trip-planner-db-data:/data \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
alpine sh -c "cd / && tar xzf /backup/db-backup.tar.gz"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
The container includes a health check endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container health
|
||||||
|
docker inspect trip-planner-production | grep -A 5 Health
|
||||||
|
|
||||||
|
# Manual health check
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
# Should return: healthy
|
||||||
|
|
||||||
|
# Check specific services
|
||||||
|
docker exec trip-planner-production supervisorctl status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing Services
|
||||||
|
|
||||||
|
When the container is running:
|
||||||
|
|
||||||
|
- **Application**: http://localhost:8080 (or your configured `APP_PORT`)
|
||||||
|
- **Health Check**: http://localhost:8080/health
|
||||||
|
- **API**: http://localhost:8080/api/*
|
||||||
|
|
||||||
|
### Internal Services (not exposed)
|
||||||
|
|
||||||
|
These services run inside the container and are not accessible from outside:
|
||||||
|
- MariaDB: `127.0.0.1:3306`
|
||||||
|
- Redis: `127.0.0.1:6379`
|
||||||
|
- PHP-FPM: `127.0.0.1:9000`
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker compose -f docker-compose.prod.yml logs -f
|
||||||
|
|
||||||
|
# Specific service logs via supervisord
|
||||||
|
docker exec trip-planner-production supervisorctl tail -f nginx
|
||||||
|
docker exec trip-planner-production supervisorctl tail -f php-fpm
|
||||||
|
docker exec trip-planner-production supervisorctl tail -f mariadb
|
||||||
|
|
||||||
|
# Laravel logs
|
||||||
|
docker exec trip-planner-production tail -f /var/www/html/storage/logs/laravel.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execute Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Laravel Artisan
|
||||||
|
docker exec trip-planner-production php artisan <command>
|
||||||
|
|
||||||
|
# Examples:
|
||||||
|
docker exec trip-planner-production php artisan migrate:status
|
||||||
|
docker exec trip-planner-production php artisan cache:clear
|
||||||
|
docker exec trip-planner-production php artisan queue:work # Run queue worker
|
||||||
|
|
||||||
|
# Database access
|
||||||
|
docker exec -it trip-planner-production mysql -u trip_user -p trip_planner
|
||||||
|
|
||||||
|
# Shell access
|
||||||
|
docker exec -it trip-planner-production sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest image
|
||||||
|
docker pull codeberg.org/lvl0/trip-planner:latest
|
||||||
|
|
||||||
|
# Recreate container (preserves volumes)
|
||||||
|
docker compose -f docker-compose.prod.yml up -d --force-recreate
|
||||||
|
|
||||||
|
# Or specific version
|
||||||
|
docker pull codeberg.org/lvl0/trip-planner:0.2.0
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### On a VPS/Server
|
||||||
|
|
||||||
|
1. **Install Docker**:
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create deployment directory**:
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/trip-planner
|
||||||
|
cd ~/trip-planner
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create docker-compose.yml**:
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: codeberg.org/lvl0/trip-planner:latest
|
||||||
|
container_name: trip-planner
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
environment:
|
||||||
|
APP_KEY: ${APP_KEY}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
APP_URL: https://your-domain.com
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/mysql
|
||||||
|
- redis-data:/data/redis
|
||||||
|
- storage-data:/var/www/html/storage/app
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
redis-data:
|
||||||
|
storage-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Create .env file**:
|
||||||
|
```bash
|
||||||
|
echo "APP_KEY=base64:$(openssl rand -base64 32)" > .env
|
||||||
|
echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Start the application**:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Set up reverse proxy** (optional, recommended):
|
||||||
|
Use Nginx, Caddy, or Traefik to handle HTTPS.
|
||||||
|
|
||||||
|
### With Reverse Proxy (Nginx Example)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name tripplanner.example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use Certbot for HTTPS:
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d tripplanner.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
|
||||||
|
**Check logs:**
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.prod.yml logs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common issues:**
|
||||||
|
- Missing `APP_KEY`: Generate one with `php artisan key:generate`
|
||||||
|
- Port already in use: Change `APP_PORT` in docker-compose
|
||||||
|
- Insufficient memory: Allocate at least 1GB RAM
|
||||||
|
|
||||||
|
### Database initialization fails
|
||||||
|
|
||||||
|
**Manually initialize:**
|
||||||
|
```bash
|
||||||
|
docker exec -it trip-planner-production sh
|
||||||
|
mysql_install_db --user=mysql --datadir=/var/lib/mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services not responding
|
||||||
|
|
||||||
|
**Check Supervisord status:**
|
||||||
|
```bash
|
||||||
|
docker exec trip-planner-production supervisorctl status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restart a service:**
|
||||||
|
```bash
|
||||||
|
docker exec trip-planner-production supervisorctl restart nginx
|
||||||
|
docker exec trip-planner-production supervisorctl restart php-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission errors
|
||||||
|
|
||||||
|
**Fix storage permissions:**
|
||||||
|
```bash
|
||||||
|
docker exec trip-planner-production chown -R appuser:appuser /var/www/html/storage
|
||||||
|
docker exec trip-planner-production chmod -R 775 /var/www/html/storage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health check failing
|
||||||
|
|
||||||
|
**Test manually:**
|
||||||
|
```bash
|
||||||
|
docker exec trip-planner-production curl -f http://localhost/health
|
||||||
|
|
||||||
|
# Check individual services
|
||||||
|
docker exec trip-planner-production supervisorctl status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance issues
|
||||||
|
|
||||||
|
**Check resource usage:**
|
||||||
|
```bash
|
||||||
|
docker stats trip-planner-production
|
||||||
|
|
||||||
|
# Allocate more resources if needed (docker-compose)
|
||||||
|
# Add under 'app' service:
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# limits:
|
||||||
|
# memory: 2G
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Production Locally
|
||||||
|
|
||||||
|
To test the production setup alongside your dev environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production runs on port 8080 (default)
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
# Dev runs on separate ports (5173, 8000, etc.)
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Both can run simultaneously without conflicts
|
||||||
|
```
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- Production: http://localhost:8080
|
||||||
|
- Development: http://localhost:5173
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Securing the Production Deployment
|
||||||
|
|
||||||
|
1. **Change default passwords** in `.env`
|
||||||
|
2. **Use strong APP_KEY** (generate with `php artisan key:generate`)
|
||||||
|
3. **Enable HTTPS** with a reverse proxy
|
||||||
|
4. **Firewall rules**: Only expose necessary ports
|
||||||
|
5. **Regular updates**: Pull latest images regularly
|
||||||
|
6. **Monitor logs**: Set up log aggregation
|
||||||
|
7. **Backup regularly**: Automate volume backups
|
||||||
|
|
||||||
|
### Environment Variable Security
|
||||||
|
|
||||||
|
**Never commit secrets to git!**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env files are gitignored
|
||||||
|
# Use a secrets manager for production
|
||||||
|
# Or use Docker secrets/Kubernetes secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
- The image includes OPcache with aggressive caching
|
||||||
|
- Static assets are cached for 1 year
|
||||||
|
- Gzip compression is enabled
|
||||||
|
- Redis handles sessions and cache
|
||||||
|
- Database is optimized for InnoDB
|
||||||
|
|
||||||
|
For high-traffic scenarios:
|
||||||
|
- Run multiple container replicas behind a load balancer
|
||||||
|
- Use external managed database (RDS, etc.)
|
||||||
|
- Use external Redis cluster
|
||||||
|
- Configure CDN for static assets
|
||||||
|
|
||||||
|
## Differences from Development
|
||||||
|
|
||||||
|
| Feature | Development | Production |
|
||||||
|
|---------|------------|------------|
|
||||||
|
| Containers | 5 separate | 1 all-in-one |
|
||||||
|
| Code | Live mounted | Baked into image |
|
||||||
|
| Frontend | Vite dev server | Pre-built static files |
|
||||||
|
| Debugging | Enabled | Disabled |
|
||||||
|
| Caching | Minimal | Aggressive |
|
||||||
|
| Security | Relaxed | Hardened |
|
||||||
|
| Size | ~2GB+ | ~800MB |
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
Images are automatically built via Woodpecker CI on Codeberg:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .woodpecker.yml extracts version from merge commits
|
||||||
|
# Example: merging release/v0.1.0 → tags 0.1.0 and latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Registry**: `codeberg.org/lvl0/trip-planner`
|
||||||
|
|
||||||
|
**Tags**:
|
||||||
|
- `latest`: Most recent build from main
|
||||||
|
- `0.1.0`, `0.2.0`, etc.: Version tags
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- Check the main [docker/README.md](../README.md)
|
||||||
|
- Review container logs: `docker logs trip-planner-production`
|
||||||
|
- Check service status: `docker exec trip-planner-production supervisorctl status`
|
||||||
|
- Inspect health: `docker inspect trip-planner-production`
|
||||||
66
docker/supervisord.conf
Normal file
66
docker/supervisord.conf
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
[unix_http_server]
|
||||||
|
file=/var/run/supervisor.sock
|
||||||
|
chmod=0700
|
||||||
|
|
||||||
|
[supervisorctl]
|
||||||
|
serverurl=unix:///var/run/supervisor.sock
|
||||||
|
|
||||||
|
[rpcinterface:supervisor]
|
||||||
|
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||||
|
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
loglevel=info
|
||||||
|
childlogdir=/var/log/supervisor
|
||||||
|
|
||||||
|
[program:mariadb]
|
||||||
|
command=/usr/bin/mysqld --user=mysql --datadir=/var/lib/mysql --bind-address=127.0.0.1 --port=3306
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=10
|
||||||
|
stopwaitsecs=30
|
||||||
|
stopsignal=TERM
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
user=mysql
|
||||||
|
|
||||||
|
[program:redis]
|
||||||
|
command=/usr/bin/redis-server --bind 127.0.0.1 --port 6379 --dir /data/redis --appendonly yes --logfile ""
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=20
|
||||||
|
stopwaitsecs=10
|
||||||
|
stopsignal=TERM
|
||||||
|
stdout_logfile=/var/log/supervisor/redis.log
|
||||||
|
stdout_logfile_maxbytes=10MB
|
||||||
|
stderr_logfile=/var/log/supervisor/redis-error.log
|
||||||
|
stderr_logfile_maxbytes=10MB
|
||||||
|
|
||||||
|
[program:php-fpm]
|
||||||
|
command=/usr/local/sbin/php-fpm -F
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=30
|
||||||
|
stopwaitsecs=10
|
||||||
|
stopsignal=QUIT
|
||||||
|
stdout_logfile=/var/log/supervisor/php-fpm.log
|
||||||
|
stdout_logfile_maxbytes=10MB
|
||||||
|
stderr_logfile=/var/log/supervisor/php-fpm-error.log
|
||||||
|
stderr_logfile_maxbytes=10MB
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=/usr/sbin/nginx -g "daemon off;"
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=40
|
||||||
|
stopwaitsecs=10
|
||||||
|
stopsignal=QUIT
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
51
frontend/.dockerignore
Normal file
51
frontend/.dockerignore
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Development
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
|
||||||
|
# Build artifacts (will be rebuilt)
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
.editorconfig
|
||||||
|
.eslintrc*
|
||||||
|
.prettierrc*
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
Loading…
Reference in a new issue