diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 945b48ff..00000000 --- a/.dockerignore +++ /dev/null @@ -1,78 +0,0 @@ -# Version control & editors -.git -.gitattributes -.gitignore -.github -.editorconfig -.idea -.vscode -.fleet -.nova -.zed -.phpactor.json - -# Dependencies (installed inside the image) -vendor -node_modules -maizzle/node_modules - -# Build artifacts (generated inside the image) -public/build -public/hot -public/storage -bootstrap/ssr -bootstrap/cache/*.php - -# Generated route/action helpers (regenerated by Wayfinder at build time) -resources/js/actions -resources/js/routes -resources/js/wayfinder - -# Runtime storage (mounted at runtime; shouldn't bloat build context) -storage/logs -storage/framework/cache/data -storage/framework/sessions -storage/framework/views -storage/pail -storage/*.key -storage/oauth-*.key - -# Test caches -.phpunit.cache -.phpunit.result.cache - -# Environment files (image must not embed secrets) -.env -.env.backup -.env.local -.env.production -.env.testing - -# Composer auth -auth.json - -# Personal homelab/deploy artifacts (not part of upstream) -deploy -HOMELAB-DEPLOY-GUIDE.md - -# Compose overrides (per-developer) -compose.override.yaml -compose.override.yml -docker-compose.override.yaml -docker-compose.override.yml - -# Docs site (if present) -docs - -# OS / misc -.DS_Store -npm-debug.log -yarn-error.log -*.log - -# Plan / agent context (not relevant for build) -.claude -.mcp.json -CLAUDE.md -GEMINI.md -boost.json diff --git a/compose.override.yaml.example b/compose.override.yaml.example deleted file mode 100644 index 5b21cdb2..00000000 --- a/compose.override.yaml.example +++ /dev/null @@ -1,52 +0,0 @@ -# TryPost — Compose override example. -# -# Compose automatically merges `compose.override.yaml` (gitignored, per-developer) -# on top of `compose.yaml`. Copy this file to start customizing: -# -# cp compose.override.yaml.example compose.override.yaml -# -# Common overrides are commented below. Uncomment what you need. - -services: - app: - # Match container UID/GID to your host user (Linux contributors on - # systems where your UID isn't 1000). - # build: - # args: - # UID: 1001 - # GID: 1001 - - # Enable Xdebug (requires installing the extension via a Dockerfile - # tweak; left commented as a hint). - # environment: - # XDEBUG_MODE: debug - # XDEBUG_CONFIG: 'client_host=host.docker.internal' - - # Forward additional ports if 8000/5173/8080 conflict with another - # service on your host. - # ports: - # - '9000:80' - - pgsql: - # Free up host port 5432 (e.g. when running another Postgres locally). - # ports: - # - '54320:5432' - - redis: - # ports: - # - '63790:6379' - - mailpit: - # Move the Mailpit UI off port 8025. - # ports: - # - '8025:8025' - # - '1025:1025' - -# Add additional services here — e.g. a Selenium container for Dusk tests: -# -# selenium: -# image: selenium/standalone-chromium -# extra_hosts: -# - 'host.docker.internal:host-gateway' -# volumes: -# - '/dev/shm:/dev/shm' diff --git a/compose.prod.yaml b/compose.prod.yaml deleted file mode 100644 index 929d5cb5..00000000 --- a/compose.prod.yaml +++ /dev/null @@ -1,189 +0,0 @@ -# TryPost — self-hosted production stack. -# -# 1. Generate an app key: docker compose -f compose.prod.yaml run --rm app php artisan key:generate --show -# Paste the value into APP_KEY below. -# 2. Edit APP_URL and the passwords marked "change me". -# 3. Start: docker compose -f compose.prod.yaml up -d -# -# This pulls the published image — no local build. Postgres, Redis, the queue -# workers, the scheduler and the WebSocket server all run for you. - -services: - app: - image: ghcr.io/trypostit/trypost:latest - container_name: trypost - restart: unless-stopped - environment: - # ===== Required ===== - APP_NAME: TryPost - APP_ENV: production - APP_DEBUG: "false" - APP_KEY: "" # <- run key:generate (see header) and paste here - APP_URL: http://localhost:8000 # <- your public URL, e.g. https://post.yourdomain.com - SELF_HOSTED: "true" - ALLOW_MULTIPLE_SOCIAL_ACCOUNTS: "true" - TRYPOST_TARGET: production - - # ===== Database (bundled postgres service below) ===== - DB_CONNECTION: pgsql - DB_HOST: pgsql - DB_PORT: "5432" - DB_DATABASE: trypost - DB_USERNAME: trypost - DB_PASSWORD: trypost-password # <- change me (must match POSTGRES_PASSWORD below) - - # ===== Redis / queue / cache / broadcasting ===== - REDIS_HOST: redis - REDIS_PORT: "6379" - QUEUE_CONNECTION: redis - CACHE_STORE: redis - SESSION_DRIVER: database - BROADCAST_CONNECTION: reverb - - # ===== WebSockets (Reverb) ===== - # The published image bakes the client at localhost:8080. For a custom - # domain, rebuild the image with --build-arg VITE_REVERB_HOST= - # VITE_REVERB_PORT=443 VITE_REVERB_SCHEME=https. - REVERB_APP_ID: "1001" - REVERB_APP_KEY: trypost-reverb-key # must match the key baked into the published image - REVERB_APP_SECRET: change-me-reverb-secret # <- change me - REVERB_HOST: localhost - REVERB_PORT: "8080" - REVERB_SCHEME: http - - # ===== Passport (OAuth / API keys / MCP) ===== - # REQUIRED for any durable deploy (and always for multi-node / load - # balancers). File keys under storage/ are NOT persisted by the volumes - # below — without these env vars a container recreate issues new keys and - # invalidates every API/MCP token. Generate once: - # docker compose -f compose.prod.yaml run --rm app php artisan passport:keys --show - # then paste the PEM contents here (use \n for newlines). - PASSPORT_PRIVATE_KEY: "" - PASSPORT_PUBLIC_KEY: "" - - # ===== Storage ===== - # Default: local disk, persisted in the "storage" volume below. - FILESYSTEM_DISK: public - # - # --- Cloudflare R2: set FILESYSTEM_DISK=r2 and fill these --- - # R2_ACCESS_KEY_ID: "" - # R2_SECRET_ACCESS_KEY: "" - # R2_ENDPOINT: "" - # R2_BUCKET: "" - # R2_URL: "" # public bucket URL (needed for media to display) - # - # --- AWS S3: set FILESYSTEM_DISK=s3 and fill these --- - # AWS_ACCESS_KEY_ID: "" - # AWS_SECRET_ACCESS_KEY: "" - # AWS_DEFAULT_REGION: us-east-1 - # AWS_BUCKET: "" - # AWS_URL: "" - - # ===== Mail ===== - # Defaults to "log" (emails written to the container log). Configure SMTP - # for real password-reset / team-invite emails. - MAIL_MAILER: log - MAIL_FROM_ADDRESS: hello@example.com - MAIL_FROM_NAME: TryPost - # MAIL_MAILER: smtp - # MAIL_HOST: "" - # MAIL_PORT: "587" - # MAIL_USERNAME: "" - # MAIL_PASSWORD: "" - # MAIL_SCHEME: tls - - # ===== Social platforms (fill in when you connect each network) ===== - # Redirect URI in each portal: ${APP_URL}/accounts//callback - # LINKEDIN_CLIENT_ID: "" - # LINKEDIN_CLIENT_SECRET: "" - # X_CLIENT_ID: "" - # X_CLIENT_SECRET: "" - # FACEBOOK_CLIENT_ID: "" - # FACEBOOK_CLIENT_SECRET: "" - # INSTAGRAM_CLIENT_ID: "" - # INSTAGRAM_CLIENT_SECRET: "" - # THREADS_CLIENT_ID: "" - # THREADS_CLIENT_SECRET: "" - # TIKTOK_CLIENT_ID: "" - # TIKTOK_CLIENT_SECRET: "" - # PINTEREST_CLIENT_ID: "" - # PINTEREST_CLIENT_SECRET: "" - # GOOGLE_CLIENT_ID: "" # YouTube + Google login - # GOOGLE_CLIENT_SECRET: "" - - # ===== AI (optional — leave blank to disable AI features) ===== - # OPENAI_API_KEY: "" - # ANTHROPIC_API_KEY: "" - # GEMINI_API_KEY: "" - # OPENROUTER_API_KEY: "" - # ELEVENLABS_API_KEY: "" - ports: - - "8000:80" # app (nginx) - - "8080:8080" # Reverb WebSocket - volumes: - - storage:/var/www/html/storage/app - depends_on: - pgsql: - condition: service_healthy - redis: - condition: service_healthy - - pgsql: - image: postgres:16-alpine - container_name: trypost-pgsql - restart: unless-stopped - environment: - POSTGRES_DB: trypost - POSTGRES_USER: trypost - POSTGRES_PASSWORD: trypost-password # <- must match DB_PASSWORD above - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U trypost -d trypost'] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: trypost-redis - restart: unless-stopped - command: redis-server --appendonly yes - volumes: - - redisdata:/data - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 10s - timeout: 3s - retries: 5 - - # Optional reverse proxy with automatic HTTPS (Let's Encrypt). - # To serve on a domain: - # 1. Point the domain's DNS at this host. - # 2. Set APP_DOMAIN below and APP_URL above to https://. - # 3. Start with the proxy profile: - # docker compose -f compose.prod.yaml --profile proxy up -d - # Without the profile, the app is served directly on http://localhost:8000. - caddy: - image: caddy:2-alpine - container_name: trypost-caddy - restart: unless-stopped - profiles: [proxy] - environment: - APP_DOMAIN: post.example.com # <- your domain - ports: - - "80:80" - - "443:443" - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy-data:/data - - caddy-config:/config - depends_on: - - app - -volumes: - pgdata: - redisdata: - storage: - caddy-data: - caddy-config: diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 244baa04..00000000 --- a/compose.yaml +++ /dev/null @@ -1,83 +0,0 @@ -services: - app: - build: - context: . - dockerfile: docker/Dockerfile - target: dev - args: - UID: ${UID:-1000} - GID: ${GID:-1000} - image: trypost-app:dev - ports: - - '${APP_PORT:-8000}:80' - - '${REVERB_PORT:-8080}:8080' - - '${VITE_PORT:-5173}:5173' - environment: - TRYPOST_DOCKER_BOOTSTRAP: '1' - UID: ${UID:-1000} - GID: ${GID:-1000} - # Real env vars take precedence over .env.testing (which pins - # DB_HOST=127.0.0.1, DB_USERNAME=root). Without these, the test - # suite can't see Postgres / Redis from inside the container. - DB_HOST: pgsql - DB_USERNAME: postgres - REDIS_HOST: redis - volumes: - - .:/var/www/html - - app-vendor:/var/www/html/vendor - - app-node-modules:/var/www/html/node_modules - extra_hosts: - - 'host.docker.internal:host-gateway' - depends_on: - pgsql: - condition: service_healthy - redis: - condition: service_healthy - healthcheck: - test: ['CMD', 'curl', '-fsS', 'http://127.0.0.1/up'] - interval: 30s - timeout: 5s - retries: 5 - start_period: 90s - - pgsql: - image: postgres:16-alpine - environment: - POSTGRES_DB: ${DB_DATABASE:-trypost} - POSTGRES_USER: ${DB_USERNAME:-postgres} - POSTGRES_PASSWORD: ${DB_PASSWORD:-password} - volumes: - - pgdata:/var/lib/postgresql/data - - ./docker/postgres-init.sh:/docker-entrypoint-initdb.d/10-create-test-db.sh:ro - ports: - - '${FORWARD_DB_PORT:-5432}:5432' - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_DATABASE:-trypost}'] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - command: redis-server --appendonly yes - volumes: - - redisdata:/data - ports: - - '${FORWARD_REDIS_PORT:-6379}:6379' - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 10s - timeout: 3s - retries: 5 - - mailpit: - image: axllent/mailpit:latest - ports: - - '${FORWARD_MAILPIT_SMTP_PORT:-1025}:1025' - - '${FORWARD_MAILPIT_UI_PORT:-8025}:8025' - -volumes: - pgdata: - redisdata: - app-vendor: - app-node-modules: diff --git a/composer.json b/composer.json index e86d17de..268a5f70 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,6 @@ "fakerphp/faker": "^1.23", "laravel/pail": "^1.2.2", "laravel/pint": "^1.24", - "laravel/sail": "*", "laravel/telescope": "^5.19", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", diff --git a/docker/.env.docker.example b/docker/.env.docker.example deleted file mode 100644 index 128d437b..00000000 --- a/docker/.env.docker.example +++ /dev/null @@ -1,222 +0,0 @@ -APP_NAME="TryPost" -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost:8000 - -# Public base URL inbound webhooks (e.g. Telegram) are registered on. -# Defaults to APP_URL; set a tunnel URL (e.g. ngrok) for local development. -WEBHOOK_URL= - -# Self-hosted mode (skips payment requirements) -SELF_HOSTED=true - -# Allow more than one connected account per social network in a workspace. -ALLOW_MULTIPLE_SOCIAL_ACCOUNTS=true - -TELESCOPE_ENABLED=false - -APP_LOCALE=en -APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US - -APP_MAINTENANCE_DRIVER=file - -BCRYPT_ROUNDS=12 - -LOG_CHANNEL=stack -LOG_STACK=single -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -# Database — service name "pgsql" inside the Compose network. -DB_CONNECTION=pgsql -DB_HOST=pgsql -DB_PORT=5432 -DB_DATABASE=trypost -DB_USERNAME=postgres -DB_PASSWORD=password - -# Session -SESSION_DRIVER=database -SESSION_LIFETIME=1440 -SESSION_ENCRYPT=false -SESSION_PATH=/ -SESSION_DOMAIN=null - -# Broadcasting, Queue, Cache — all Redis-backed in Docker. -BROADCAST_CONNECTION=reverb -QUEUE_CONNECTION=redis -CACHE_STORE=redis - -# File Storage — local-disk by default; switch to s3/r2 for production. -FILESYSTEM_DISK=local - -# Redis — service name "redis" inside the Compose network. -REDIS_HOST=redis -REDIS_PASSWORD=null -REDIS_PORT=6379 - -# Mail — service name "mailpit" inside the Compose network. UI at :8025. -MAIL_MAILER=smtp -MAIL_HOST=mailpit -MAIL_PORT=1025 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" - -# Reverb (WebSockets) — host=localhost is browser-perspective via host port-forward. -REVERB_APP_ID=1001 -REVERB_APP_KEY=trypost-reverb-key -REVERB_APP_SECRET=trypost-reverb-secret -REVERB_HOST=localhost -REVERB_PORT=8080 -REVERB_SCHEME=http - -# AWS S3 (set FILESYSTEM_DISK=s3) -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_URL= - -# Cloudflare R2 (set FILESYSTEM_DISK=r2) -R2_ACCESS_KEY_ID= -R2_SECRET_ACCESS_KEY= -R2_ENDPOINT= -R2_REGION=auto -R2_BUCKET= -R2_URL= - -# ============================================ -# Social Platform Credentials (optional in dev) -# ============================================ -LINKEDIN_CLIENT_ID= -LINKEDIN_CLIENT_SECRET= -LINKEDIN_CLIENT_REDIRECT="${APP_URL}/accounts/linkedin/callback" -# LINKEDIN_SCOPES="openid,profile,email,w_member_social" -LINKEDIN_PAGE_CLIENT_REDIRECT="${APP_URL}/accounts/linkedin-page/callback" -# LINKEDIN_PAGE_SCOPES="openid,profile,email,w_organization_social,r_organization_social,rw_organization_admin,w_member_social" - -X_CLIENT_ID= -X_CLIENT_SECRET= -X_CLIENT_REDIRECT="${APP_URL}/accounts/x/callback" - -TIKTOK_CLIENT_ID= -TIKTOK_CLIENT_SECRET= -TIKTOK_CLIENT_REDIRECT="${APP_URL}/accounts/tiktok/callback" - -FACEBOOK_CLIENT_ID= -FACEBOOK_CLIENT_SECRET= -FACEBOOK_CLIENT_REDIRECT="${APP_URL}/accounts/facebook/callback" - -INSTAGRAM_CLIENT_ID= -INSTAGRAM_CLIENT_SECRET= -INSTAGRAM_CLIENT_REDIRECT="${APP_URL}/accounts/instagram/callback" - -THREADS_CLIENT_ID= -THREADS_CLIENT_SECRET= -THREADS_CLIENT_REDIRECT="${APP_URL}/accounts/threads/callback" - -GOOGLE_AUTH_ENABLED=false -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -GOOGLE_CLIENT_REDIRECT="${APP_URL}/accounts/youtube/callback" -GOOGLE_AUTH_CALLBACK="${APP_URL}/auth/google/callback" - -GITHUB_AUTH_ENABLED=false -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GITHUB_AUTH_CALLBACK="${APP_URL}/auth/github/callback" - -PINTEREST_CLIENT_ID= -PINTEREST_CLIENT_SECRET= -PINTEREST_CLIENT_REDIRECT="${APP_URL}/accounts/pinterest/callback" - -# Telegram (single shared bot — create one via https://t.me/BotFather) -# After setting these, run: php artisan telegram:set-webhook -TELEGRAM_BOT_TOKEN= -TELEGRAM_BOT_USERNAME= -TELEGRAM_WEBHOOK_SECRET= - -# AI Services -OPENAI_API_KEY= -ANTHROPIC_API_KEY= -GEMINI_API_KEY= -OPENROUTER_API_KEY= -ELEVENLABS_API_KEY= -# Only needed if you point one of the AI_*_PROVIDER vars below at these providers. -# XAI_API_KEY= -# GROQ_API_KEY= -# MISTRAL_API_KEY= -# DEEPSEEK_API_KEY= -# Ollama runs locally and needs no key — from inside Docker point it at the host, not localhost. -# OLLAMA_URL=http://host.docker.internal:11434 -# Any OpenAI-compatible endpoint (LM Studio, vLLM, LocalAI, ...) via AI_TEXT_PROVIDER=openai-compatible. -# It has no built-in default model, so OPENAI_COMPATIBLE_TEXT_MODEL is required for that provider. -# OPENAI_COMPATIBLE_URL= -# OPENAI_COMPATIBLE_API_KEY= -# OPENAI_COMPATIBLE_TEXT_MODEL= - -# AI Provider Selection -# text: openai | anthropic | gemini | openrouter | xai | groq | mistral | deepseek | ollama | ... -# image: openai | gemini | xai | openrouter | ... -# audio: openai | elevenlabs | gemini | openrouter | ... -# OpenRouter is a first-class laravel/ai provider (AI_TEXT_PROVIDER=openrouter + OPENROUTER_API_KEY). -AI_TEXT_PROVIDER=openai -AI_IMAGE_PROVIDER=openai -AI_AUDIO_PROVIDER=elevenlabs - -# AI Model Overrides (optional) -# Each provider picks a sensible default model per capability on its own - -# these are NOT shared across capabilities, so overriding one (e.g. a text -# model) never affects another (e.g. that same provider's image model). -# Uncomment only the ones you want to override; every provider in play above -# (AI_TEXT_PROVIDER / AI_IMAGE_PROVIDER / AI_AUDIO_PROVIDER) reads its own set. -# The values below are laravel/ai's current defaults, shown as format examples - -# they move with the package, so don't treat them as a contract. -# OPENAI_TEXT_MODEL=gpt-5.4 -# OPENAI_IMAGE_MODEL=gpt-image-2 -# OPENAI_AUDIO_MODEL=gpt-4o-mini-tts -# ANTHROPIC_TEXT_MODEL=claude-sonnet-5 -# GEMINI_TEXT_MODEL=gemini-3.6-flash -# GEMINI_IMAGE_MODEL=gemini-3.1-flash-image-preview -# GEMINI_AUDIO_MODEL=gemini-2.5-flash-preview-tts -# XAI_TEXT_MODEL=grok-4.20-non-reasoning -# XAI_IMAGE_MODEL=grok-imagine-image -# OPENROUTER_TEXT_MODEL=anthropic/claude-sonnet-4.6 -# OPENROUTER_IMAGE_MODEL=google/gemini-3.1-flash-image-preview -# OPENROUTER_AUDIO_MODEL=google/gemini-3.1-flash-tts-preview -# ELEVENLABS_AUDIO_MODEL=eleven_multilingual_v2 -# GROQ_TEXT_MODEL=openai/gpt-oss-120b -# MISTRAL_TEXT_MODEL=mistral-medium-latest -# DEEPSEEK_TEXT_MODEL=deepseek-v4-flash -# OLLAMA_TEXT_MODEL=llama3.1:8b -# Ollama has no image/audio-capable driver in laravel/ai - text only. - -# Telemetry — disabled by default in Docker dev. -NIGHTWATCH_ENABLED=false -NIGHTWATCH_TOKEN= - -# Media Services -UNSPLASH_ACCESS_KEY= -UNSPLASH_SECRET_KEY= -GIPHY_API_KEY= - -# Analytics (optional) -GTM_ID= -POSTHOG_ENABLED=false -POSTHOG_API_KEY= -POSTHOG_HOST=https://us.i.posthog.com - -# Vite -VITE_APP_NAME="${APP_NAME}" -VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" -VITE_REVERB_HOST="${REVERB_HOST}" -VITE_REVERB_PORT="${REVERB_PORT}" -VITE_REVERB_SCHEME="${REVERB_SCHEME}" -VITE_POSTHOG_ENABLED="${POSTHOG_ENABLED}" -VITE_POSTHOG_API_KEY="${POSTHOG_API_KEY}" -VITE_POSTHOG_HOST="${POSTHOG_HOST}" diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 66e9d2a5..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,214 +0,0 @@ -# syntax=docker/dockerfile:1.7 - -# ---------------------------------------------------------------------------- -# TryPost Dockerfile — multi-stage with `dev` and `production` targets. -# -# docker build --target dev -t trypost:dev -f docker/Dockerfile . -# docker build --target production -t trypost:prod -f docker/Dockerfile . -# ---------------------------------------------------------------------------- - -ARG PHP_VERSION=8.4 -ARG NODE_VERSION=22 - -# ---------------------------------------------------------------------------- -# Stage 1: system-base — PHP-FPM + system packages + extensions -# ---------------------------------------------------------------------------- -FROM php:${PHP_VERSION}-fpm-alpine AS system-base - -RUN apk add --no-cache \ - nginx \ - supervisor \ - bash \ - curl \ - git \ - unzip \ - shadow \ - tzdata \ - postgresql-client \ - postgresql-dev \ - mysql-client \ - libpng-dev \ - libjpeg-turbo-dev \ - freetype-dev \ - libwebp-dev \ - libavif-dev \ - libzip-dev \ - oniguruma-dev \ - icu-dev \ - linux-headers \ - $PHPIZE_DEPS \ - && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp --with-avif \ - && docker-php-ext-install -j"$(nproc)" \ - pdo_pgsql \ - pgsql \ - pdo_mysql \ - gd \ - zip \ - opcache \ - bcmath \ - exif \ - pcntl \ - intl \ - sockets \ - && pecl install redis \ - && docker-php-ext-enable redis \ - && apk del $PHPIZE_DEPS \ - && rm -rf /tmp/* /var/cache/apk/* - -COPY --from=composer:2 /usr/bin/composer /usr/bin/composer - -WORKDIR /var/www/html - -# ---------------------------------------------------------------------------- -# Stage 2: composer-deps — full PHP deps (incl. dev) for tooling/asset build -# ---------------------------------------------------------------------------- -FROM system-base AS composer-deps - -COPY composer.json composer.lock ./ -RUN composer install \ - --no-scripts \ - --no-autoloader \ - --prefer-dist \ - --no-interaction - -# ---------------------------------------------------------------------------- -# Stage 3: composer-deps-prod — production deps only (no dev tooling) -# ---------------------------------------------------------------------------- -FROM system-base AS composer-deps-prod - -COPY composer.json composer.lock ./ -RUN composer install \ - --no-dev \ - --no-scripts \ - --no-autoloader \ - --prefer-dist \ - --no-interaction - -# ---------------------------------------------------------------------------- -# Stage 4: asset-build — wayfinder TS gen + Vite + Inertia SSR build -# -# PHP and Node together in one stage because @laravel/vite-plugin-wayfinder -# invokes `php artisan wayfinder:generate` during vite build — splitting -# the stages would mean the vite phase has no PHP available. -# -# Vite inlines VITE_* values into the JS bundle at build time. Pass them -# as build args (--build-arg VITE_REVERB_APP_KEY=...) so the bundle reaches -# the browser with the correct Reverb/PostHog config. Runtime env vars on -# the container have no effect on the already-compiled bundle. -# ---------------------------------------------------------------------------- -FROM composer-deps AS asset-build - -# Node 22 for Vite + npm scripts. -RUN apk add --no-cache nodejs npm - -ARG VITE_APP_NAME=TryPost -ARG VITE_REVERB_APP_KEY= -ARG VITE_REVERB_HOST=localhost -ARG VITE_REVERB_PORT=8080 -ARG VITE_REVERB_SCHEME=http -ARG VITE_POSTHOG_ENABLED=false -ARG VITE_POSTHOG_API_KEY= -ARG VITE_POSTHOG_HOST=https://us.i.posthog.com - -# Stub PHP envs so artisan can boot (overridden at runtime). -ENV APP_KEY=base64:c3R1Yi13YXlmaW5kZXItZ2VuLWtleS1mb3ItYnVpbGRpbmctYXNzZXRzMA== \ - APP_ENV=production \ - APP_DEBUG=false \ - APP_URL=http://localhost \ - VITE_APP_NAME=${VITE_APP_NAME} \ - VITE_REVERB_APP_KEY=${VITE_REVERB_APP_KEY} \ - VITE_REVERB_HOST=${VITE_REVERB_HOST} \ - VITE_REVERB_PORT=${VITE_REVERB_PORT} \ - VITE_REVERB_SCHEME=${VITE_REVERB_SCHEME} \ - VITE_POSTHOG_ENABLED=${VITE_POSTHOG_ENABLED} \ - VITE_POSTHOG_API_KEY=${VITE_POSTHOG_API_KEY} \ - VITE_POSTHOG_HOST=${VITE_POSTHOG_HOST} - -COPY --from=composer-deps /var/www/html/vendor ./vendor -COPY . . - -# Ensure Laravel runtime dirs exist — .dockerignore strips their contents -# (correct: caches shouldn't bloat builds), but artisan boot needs the dirs. -RUN mkdir -p storage/framework/cache/data \ - storage/framework/sessions \ - storage/framework/views \ - storage/logs \ - bootstrap/cache - -RUN composer dump-autoload --no-scripts --optimize \ - && php artisan wayfinder:generate --with-form \ - && npm ci --no-audit --no-fund \ - && npm run build \ - && npm run build:ssr - -# ---------------------------------------------------------------------------- -# Stage 6: dev — local development image (bind-mount the source at runtime) -# ---------------------------------------------------------------------------- -FROM system-base AS dev - -ARG UID=1000 -ARG GID=1000 - -# Node + npm for in-container Vite, npm scripts, ad-hoc tooling. -RUN apk add --no-cache nodejs npm - -# Create non-root app user matching host UID/GID for clean bind-mount writes. -RUN groupmod -g "${GID}" www-data 2>/dev/null || groupadd -g "${GID}" app \ - && (id -u app >/dev/null 2>&1 || useradd -u "${UID}" -g "${GID}" -d /home/app -m -s /bin/bash app) \ - && chown -R "${UID}:${GID}" /var/www/html - -COPY docker/nginx.conf /etc/nginx/http.d/default.conf -COPY docker/php.dev.ini /usr/local/etc/php/conf.d/99-trypost.ini -COPY docker/supervisord.dev.conf /etc/supervisor/conf.d/supervisord.conf -COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh -COPY docker/.env.docker.example /var/www/html/.env.docker.example -RUN chmod +x /usr/local/bin/entrypoint.sh - -ENV TRYPOST_TARGET=dev - -EXPOSE 80 5173 8080 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \ - CMD curl -fsS http://127.0.0.1/up || exit 1 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] - -# ---------------------------------------------------------------------------- -# Stage 7: production — self-contained image for self-hosters -# ---------------------------------------------------------------------------- -FROM system-base AS production - -# php-fpm runs as www-data (Alpine default pool config) — that's the user -# storage and bootstrap/cache must be owned by, so Laravel can write logs, -# session files, view cache, etc. - -COPY docker/nginx.conf /etc/nginx/http.d/default.conf -COPY docker/php.prod.ini /usr/local/etc/php/conf.d/99-trypost.ini -COPY docker/supervisord.prod.conf /etc/supervisor/conf.d/supervisord.conf -COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh - -# Application source + generated wayfinder TS + built assets — all from -# asset-build (the merged stage that ran php artisan wayfinder:generate, -# npm run build, and npm run build:ssr). -COPY --from=asset-build /var/www/html /var/www/html - -# Replace the dev-equipped vendor/ with production-only deps. composer-deps-prod -# is built independently and is much smaller (no phpunit, no debugbar, no pail, -# no pint). Also wipe bootstrap/cache/*.php — package:discover ran with dev -# deps and recorded providers (Pail, Telescope etc) that no longer exist; -# Laravel rediscovers cleanly at first boot. -RUN rm -rf /var/www/html/vendor /var/www/html/bootstrap/cache/*.php -COPY --from=composer-deps-prod /var/www/html/vendor /var/www/html/vendor - -RUN composer dump-autoload --optimize --classmap-authoritative --no-scripts \ - && chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache - -ENV TRYPOST_TARGET=production - -EXPOSE 80 8080 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD curl -fsS http://127.0.0.1/up || exit 1 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100755 index 8fdc403a..00000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/bin/sh -# TryPost — container entrypoint. Idempotent first-run setup, then exec supervisord. - -set -e - -cd /var/www/html - -TARGET="${TRYPOST_TARGET:-dev}" - -# One-off commands from `docker compose run app ...` must bypass the long-lived -# application bootstrap and execute exactly as requested. -if [ "$#" -gt 0 ]; then - exec "$@" -fi - -if [ "${TARGET}" = "production" ] && [ -z "${APP_KEY:-}" ]; then - echo "[entrypoint] APP_KEY is required in production" >&2 - exit 1 -fi - -# 1) Bootstrap .env from the Docker template on first dev boot. The bind-mount -# in dev hides /var/www/html/.env.docker.example, so prefer docker/ first. -if [ "${TRYPOST_DOCKER_BOOTSTRAP:-0}" = "1" ] && [ ! -f .env ]; then - if [ -f docker/.env.docker.example ]; then - echo "[entrypoint] seeding .env from docker/.env.docker.example" - cp docker/.env.docker.example .env - elif [ -f .env.docker.example ]; then - echo "[entrypoint] seeding .env from .env.docker.example" - cp .env.docker.example .env - fi - # Hand the seeded .env over to the host user so they can edit it. - chown "${UID:-1000}:${GID:-1000}" .env 2>/dev/null || true -fi - -# 2) Skip-bootstrap escape hatch for advanced users. -if [ "${TRYPOST_SKIP_BOOTSTRAP:-0}" = "1" ]; then - echo "[entrypoint] TRYPOST_SKIP_BOOTSTRAP=1 — exec'ing supervisord without setup" - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf -fi - -# 3) Re-install composer deps if vendor was wiped (down -v in dev). -if [ "${TARGET}" = "dev" ] && [ ! -f vendor/autoload.php ]; then - echo "[entrypoint] vendor/ missing — running composer install" - composer install --no-interaction --prefer-dist -fi - -# 4) Re-install node_modules if empty/wiped (dev only; vite needs them). -# Anonymous volumes pre-create the dir, so check for npm's lockfile instead. -if [ "${TARGET}" = "dev" ] && [ ! -f node_modules/.package-lock.json ]; then - echo "[entrypoint] node_modules/ empty — running npm ci" - npm ci --no-audit --no-fund -fi - -# 5) APP_KEY — generate on first boot if blank. -if [ -f .env ] && ! grep -qE '^APP_KEY=base64:' .env; then - echo "[entrypoint] generating APP_KEY" - php artisan key:generate --force -fi - -# 6) Wait for Postgres to be reachable. -DB_HOST_VALUE="${DB_HOST:-pgsql}" -DB_PORT_VALUE="${DB_PORT:-5432}" -DB_USER_VALUE="${DB_USERNAME:-postgres}" -DB_NAME_VALUE="${DB_DATABASE:-trypost}" - -echo "[entrypoint] waiting for postgres at ${DB_HOST_VALUE}:${DB_PORT_VALUE}" -WAIT_ATTEMPTS=0 -until pg_isready -h "${DB_HOST_VALUE}" -p "${DB_PORT_VALUE}" -U "${DB_USER_VALUE}" -d "${DB_NAME_VALUE}" >/dev/null 2>&1; do - WAIT_ATTEMPTS=$((WAIT_ATTEMPTS + 1)) - if [ "${WAIT_ATTEMPTS}" -gt 60 ]; then - echo "[entrypoint] postgres not reachable after 60s — continuing anyway" - break - fi - sleep 1 -done - -# 7) Run migrations (graceful: succeeds even when nothing to migrate). -echo "[entrypoint] running migrations" -php artisan migrate --force - -# 8) storage:link if missing. -if [ ! -L public/storage ]; then - echo "[entrypoint] linking storage" - php artisan storage:link --force || true -fi - -# 9) Passport keys. Prefer PASSPORT_PRIVATE_KEY / PASSPORT_PUBLIC_KEY from -# the environment (required for durable / multi-node deploys — storage/oauth-* -# is not on a persisted volume in compose.prod.yaml). Fall back to generating -# files under storage/ only for local/dev when those env vars are unset. -if [ -n "${PASSPORT_PRIVATE_KEY:-}" ] && [ -n "${PASSPORT_PUBLIC_KEY:-}" ]; then - echo "[entrypoint] using Passport keys from environment" -elif [ "${TRYPOST_TARGET:-}" = "production" ] || [ "${APP_ENV:-}" = "production" ]; then - echo "[entrypoint] ERROR: PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY must be set in production." >&2 - echo "[entrypoint] Generate once with: php artisan passport:keys --show" >&2 - exit 1 -elif [ ! -f storage/oauth-private.key ] || [ ! -f storage/oauth-public.key ]; then - echo "[entrypoint] generating Passport keys (dev fallback)" - php artisan passport:keys --force -fi - -# 10) Personal access client for REST API keys. The seeder is idempotent, so -# fresh self-hosted installs and existing deployments are both safe. -echo "[entrypoint] ensuring Passport personal access client" -php artisan db:seed --class='Database\Seeders\PassportSeeder' --force - -# 11) Wayfinder TS regen — Vite needs the files before it boots. -echo "[entrypoint] regenerating wayfinder helpers" -php artisan wayfinder:generate --with-form || true - -# 12) Cache strategy: prod = pre-cache; dev = clear. -if [ "${TARGET}" = "production" ]; then - php artisan config:cache - php artisan route:cache - php artisan view:cache - php artisan event:cache -else - php artisan config:clear - php artisan route:clear - php artisan view:clear - php artisan event:clear -fi - -# 13) Permissions. Production php-fpm pool runs as www-data (Alpine default), -# so storage and bootstrap/cache must be writable by that user — Laravel -# needs to write session files, view cache, log files, etc. -if [ "${TARGET}" = "production" ]; then - chown -R www-data:www-data storage bootstrap/cache -else - # Dev: ensure UID-mapped user owns runtime dirs. - APP_UID="${UID:-1000}" - APP_GID="${GID:-1000}" - chown -R "${APP_UID}:${APP_GID}" storage bootstrap/cache 2>/dev/null || true -fi - -echo "[entrypoint] ready — handing off to supervisord" -exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/docker/nginx.conf b/docker/nginx.conf deleted file mode 100644 index 62503040..00000000 --- a/docker/nginx.conf +++ /dev/null @@ -1,56 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name _; - root /var/www/html/public; - index index.php; - - charset utf-8; - client_max_body_size 1G; - - # Reverb WebSocket (Pusher protocol). The browser connects to - # wss:///app/; the broadcaster REST API lives under /apps/. - # Both are proxied to the in-container Reverb on 127.0.0.1:8080. - location /app/ { - proxy_pass http://127.0.0.1:8080; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - 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; - proxy_read_timeout 86400s; - proxy_send_timeout 86400s; - } - - location /apps/ { - proxy_pass http://127.0.0.1:8080; - proxy_http_version 1.1; - 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; - } - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location = /favicon.ico { access_log off; log_not_found off; } - location = /robots.txt { access_log off; log_not_found off; } - - error_page 404 /index.php; - - location ~ \.php$ { - fastcgi_pass 127.0.0.1:9000; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_read_timeout 120s; - include fastcgi_params; - fastcgi_hide_header X-Powered-By; - } - - location ~ /\.(?!well-known).* { - deny all; - } -} diff --git a/docker/php.dev.ini b/docker/php.dev.ini deleted file mode 100644 index e13eaaa9..00000000 --- a/docker/php.dev.ini +++ /dev/null @@ -1,22 +0,0 @@ -; TryPost — PHP settings for local development. -memory_limit = 512M -upload_max_filesize = 1G -post_max_size = 1G -max_execution_time = 120 -max_input_vars = 5000 - -; Hot-reload PHP edits without restarting FPM. -opcache.enable = 1 -opcache.memory_consumption = 128 -opcache.max_accelerated_files = 10000 -opcache.validate_timestamps = 1 -opcache.revalidate_freq = 0 - -; Surface errors loudly in dev. -display_errors = On -display_startup_errors = On -error_reporting = E_ALL -log_errors = On -error_log = /proc/self/fd/2 - -; Xdebug optional — enable via XDEBUG_MODE in compose.override.yaml. diff --git a/docker/php.prod.ini b/docker/php.prod.ini deleted file mode 100644 index 8bca98c6..00000000 --- a/docker/php.prod.ini +++ /dev/null @@ -1,20 +0,0 @@ -; TryPost — PHP settings for production. -memory_limit = 256M -upload_max_filesize = 1G -post_max_size = 1G -max_execution_time = 60 -max_input_vars = 5000 - -; OpCache hardened for production: no timestamp checks, larger interned-string buffer. -opcache.enable = 1 -opcache.memory_consumption = 192 -opcache.max_accelerated_files = 20000 -opcache.validate_timestamps = 0 -opcache.interned_strings_buffer = 16 - -; Security -expose_php = Off -display_errors = Off -display_startup_errors = Off -log_errors = On -error_log = /proc/self/fd/2 diff --git a/docker/postgres-init.sh b/docker/postgres-init.sh deleted file mode 100755 index 5d3d2ad8..00000000 --- a/docker/postgres-init.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# Creates the test database expected by phpunit.xml on first Postgres boot. -# Postgres-alpine runs every *.sh / *.sql in /docker-entrypoint-initdb.d/ once, -# right after the primary database (POSTGRES_DB) is initialized. - -set -e - -psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL - CREATE DATABASE trypost_test; - GRANT ALL PRIVILEGES ON DATABASE trypost_test TO "${POSTGRES_USER}"; -EOSQL diff --git a/docker/supervisord.dev.conf b/docker/supervisord.dev.conf deleted file mode 100644 index 983aaa09..00000000 --- a/docker/supervisord.dev.conf +++ /dev/null @@ -1,69 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/dev/null -logfile_maxbytes=0 -pidfile=/run/supervisord.pid - -[program:php-fpm] -command=php-fpm -F -autostart=true -autorestart=true -priority=10 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:nginx] -command=nginx -g 'daemon off;' -autostart=true -autorestart=true -priority=20 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:reverb] -command=php /var/www/html/artisan reverb:start --host=0.0.0.0 --port=8080 -autostart=true -autorestart=true -priority=30 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:horizon] -command=php /var/www/html/artisan horizon -autostart=true -autorestart=true -priority=30 -stopwaitsecs=10 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:scheduler] -command=php /var/www/html/artisan schedule:work -autostart=true -autorestart=true -priority=40 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:vite] -command=/bin/sh -c "exec /usr/bin/npm run dev -- --host 0.0.0.0" -directory=/var/www/html -autostart=true -autorestart=true -startsecs=10 -priority=50 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 diff --git a/docker/supervisord.prod.conf b/docker/supervisord.prod.conf deleted file mode 100644 index 95e29ddb..00000000 --- a/docker/supervisord.prod.conf +++ /dev/null @@ -1,58 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/dev/null -logfile_maxbytes=0 -pidfile=/run/supervisord.pid - -[program:php-fpm] -command=php-fpm -F -autostart=true -autorestart=true -priority=10 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:nginx] -command=nginx -g 'daemon off;' -autostart=true -autorestart=true -priority=20 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:reverb] -command=php /var/www/html/artisan reverb:start --host=0.0.0.0 --port=8080 -autostart=true -autorestart=unexpected -exitcodes=0,2 -priority=30 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:horizon] -command=php /var/www/html/artisan horizon -autostart=true -autorestart=true -priority=30 -stopwaitsecs=10 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:scheduler] -command=php /var/www/html/artisan schedule:work -autostart=true -autorestart=true -priority=40 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0