diff --git a/.dockerignore b/.dockerignore index 65c8a94..8c67722 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,10 @@ node_modules/ npm-debug.log* vendor/ +# Produced by the builder stages; never copy a local build in. +public/build/ +public/hot + .env .env.* !.env.example @@ -18,5 +22,12 @@ storage/framework/cache/* storage/framework/sessions/* storage/framework/views/* +# Generated on the host with dev dependencies present; packages.php would +# register providers (laravel/pail, sail, ...) that --no-dev never installs. +bootstrap/cache/*.php + tests/ README.md +docs/ +compose.yaml +.forgejo/ diff --git a/.env.example b/.env.example index 808d764..2045753 100644 --- a/.env.example +++ b/.env.example @@ -20,7 +20,13 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite +DB_CONNECTION=mariadb +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=anagram +DB_USERNAME=anagram +DB_PASSWORD= +DB_ROOT_PASSWORD= # DB_HOST=127.0.0.1 # DB_PORT=3306 # DB_DATABASE=laravel diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..5136735 --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build and Push Docker Image +on: + push: + branches: [main] + tags: ['v*'] + +jobs: + build: + runs-on: docker + container: + image: catthehacker/ubuntu:act-latest + steps: + - uses: https://data.forgejo.org/actions/checkout@v4 + + - name: Set up Docker Buildx + uses: https://data.forgejo.org/docker/setup-buildx-action@v3 + + - name: Login to Forgejo Registry + uses: https://data.forgejo.org/docker/login-action@v3 + with: + registry: forge.lvl0.xyz + username: ${{ github.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Determine tags + id: meta + run: | + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + TAG="${{ github.ref_name }}" + echo "tags=forge.lvl0.xyz/anagram-finder/web:${TAG},forge.lvl0.xyz/anagram-finder/web:latest" >> $GITHUB_OUTPUT + else + echo "tags=forge.lvl0.xyz/anagram-finder/web:latest" >> $GITHUB_OUTPUT + fi + + - name: Build and push + uses: https://data.forgejo.org/docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index ae67c4d..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - cooldown: - default-days: 5 - groups: - github-actions: - patterns: - - "*" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 7fbaaf7..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: tests - -on: - push: - branches: - - main - pull_request: - -permissions: - contents: read - -jobs: - ci: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 - with: - php-version: '8.3' - tools: composer:v2 - coverage: none - - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '22' - - - name: Setup Application - run: composer setup - - - name: Run CI Checks - run: composer ci:check diff --git a/.gitignore b/.gitignore index 18e026d..3010d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ yarn-error.log /.vscode /.zed /.php-cs-fixer.cache +compose.override.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4a391e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,123 @@ +# ---- vendor ---------------------------------------------------------------- +FROM composer:2 AS vendor + +WORKDIR /app + +# anagram-finder/core is a VCS repository, so composer needs git. +RUN apk add --no-cache git + +COPY composer.json composer.lock ./ +RUN composer install \ + --no-dev \ + --no-scripts \ + --no-autoloader \ + --prefer-dist \ + --no-interaction + +COPY . . +# --no-scripts: Laravel's post-autoload-dump hook runs artisan, which wants an +# environment that does not exist at build time. Package discovery happens on boot. +RUN composer dump-autoload --no-dev --optimize --classmap-authoritative --no-scripts + +# ---- assets ---------------------------------------------------------------- +FROM node:22-alpine AS assets + +WORKDIR /app + +# @lvl0/ui is a git dependency, so npm needs git available. +RUN apk add --no-cache git + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY vite.config.js ./ +COPY resources ./resources + +# app.css imports Flux's stylesheet from vendor/ and scans three vendor globs +# for class names, so the asset build needs composer's output present. +COPY --from=vendor /app/vendor ./vendor + +RUN npm run build + +# ---- runtime --------------------------------------------------------------- +FROM dunglas/frankenphp:1-php8.4-alpine + +RUN apk add --no-cache mariadb-client curl + +RUN install-php-extensions \ + pdo_mysql \ + opcache \ + zip \ + gd \ + intl \ + bcmath \ + pcntl + +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +WORKDIR /app + +COPY --from=vendor /app /app +COPY --from=assets /app/public/build /app/public/build + +# The dev server writes public/hot and only removes it on a clean shutdown; a +# stray copy makes @vite() point at a dev server that does not exist here. +RUN rm -f /app/public/hot + +RUN cat > /etc/caddy/Caddyfile <<'EOF' +{ + frankenphp + order php_server before file_server +} + +:8000 { + root * /app/public + + php_server { + index index.php + } + + encode gzip + file_server + + header { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + Referrer-Policy "strict-origin-when-cross-origin" + } +} +EOF + +RUN cat > /start.sh <<'EOF' +#!/bin/sh +set -e + +if [ -n "$DB_HOST" ]; then + echo "Waiting for database..." + until mariadb -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" -e "SELECT 1" >/dev/null 2>&1; do + sleep 2 + done +fi + +# Rebuild the package manifest from what is actually installed. Any manifest +# baked in at build time could name a dev provider that --no-dev omitted. +rm -f bootstrap/cache/packages.php bootstrap/cache/services.php +php artisan package:discover --ansi + +php artisan migrate --force + +# Cached at boot rather than at build: config:cache bakes in environment values, +# which are not known until the container runs. +php artisan config:cache +php artisan route:cache +php artisan view:cache + +exec frankenphp run --config /etc/caddy/Caddyfile +EOF +RUN chmod +x /start.sh + +RUN chown -R www-data:www-data /app/storage /app/bootstrap/cache + +EXPOSE 8000 + +CMD ["/start.sh"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..52e6f8b --- /dev/null +++ b/compose.yaml @@ -0,0 +1,69 @@ +# Anagram Finder — self-hosting stack. +# +# Copy .env.example to .env, set APP_KEY and the database passwords, then: +# docker compose up -d +# +# The app binds to 127.0.0.1 only. Put a reverse proxy in front of it for +# anything public-facing, and set APP_URL to the address people will use. + +services: + app: + image: forge.lvl0.xyz/anagram-finder/web:latest + container_name: anagram_app + restart: unless-stopped + ports: + - "127.0.0.1:8000:8000" + environment: + APP_NAME: "Anagram Finder" + APP_ENV: production + APP_DEBUG: "false" + APP_KEY: "${APP_KEY}" + APP_URL: "${APP_URL:-http://localhost:8000}" + DB_CONNECTION: mariadb + DB_HOST: db + DB_PORT: "3306" + DB_DATABASE: "${DB_DATABASE:-anagram}" + DB_USERNAME: "${DB_USERNAME:-anagram}" + DB_PASSWORD: "${DB_PASSWORD}" + SESSION_DRIVER: database + CACHE_STORE: database + QUEUE_CONNECTION: database + # A plain dependency, not condition: service_healthy — podman-compose 1.5.0 + # hangs waiting on that. The app's start script polls for the database itself. + depends_on: + - db + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8000/up"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - anagram + + db: + image: mariadb:11 + container_name: anagram_db + restart: unless-stopped + environment: + MARIADB_DATABASE: "${DB_DATABASE:-anagram}" + MARIADB_USER: "${DB_USERNAME:-anagram}" + MARIADB_PASSWORD: "${DB_PASSWORD}" + MARIADB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - anagram + +networks: + anagram: + driver: bridge + +volumes: + db_data: diff --git a/composer.json b/composer.json index a72a7f8..67a2805 100644 --- a/composer.json +++ b/composer.json @@ -115,7 +115,7 @@ "repositories": [ { "type": "vcs", - "url": "ssh://git@forge.lvl0.xyz:2222/anagram-finder/core.git" + "url": "https://forge.lvl0.xyz/anagram-finder/core.git" } ] } diff --git a/composer.lock b/composer.lock index ede989f..d572f5b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,14 +4,14 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "421410120a87c8f8ec2456627557bc36", + "content-hash": "1b36d4e3f53a631da85cf21b05c1a2b6", "packages": [ { "name": "anagram-finder/core", "version": "v0.1.0", "source": { "type": "git", - "url": "ssh://git@forge.lvl0.xyz:2222/anagram-finder/core.git", + "url": "https://forge.lvl0.xyz/anagram-finder/core.git", "reference": "4b8ce74729fa9a6d0ca91c5d661256ca256e5291" }, "require": { diff --git a/package-lock.json b/package-lock.json index d632543..2e2cf39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "tailwindcss": "^4.0.7", "vite": "^8.0.0" }, + "devDependencies": { + "@lvl0/ui": "git+https://forge.lvl0.xyz/lvl0/ui.git#v0.1.1" + }, "optionalDependencies": { "@laravel/multiplex": "^0.4.1", "@rollup/rollup-linux-x64-gnu": "4.9.5", @@ -102,6 +105,15 @@ "node": ">=22.13.0" } }, + "node_modules/@lvl0/ui": { + "version": "0.1.0", + "resolved": "git+https://forge.lvl0.xyz/lvl0/ui.git#e235cec8a3cfe4060a4fda5857ac2bd1fc9bb7b1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": "^4.0.0" + } + }, "node_modules/@oxc-project/types": { "version": "0.146.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", diff --git a/package.json b/package.json index 8813571..b5bf88d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,6 @@ "lightningcss-linux-x64-gnu": "^1.29.1" }, "devDependencies": { - "@lvl0/ui": "git+ssh://git@forge.lvl0.xyz:2222/lvl0/ui.git#v0.1.0" + "@lvl0/ui": "git+https://forge.lvl0.xyz/lvl0/ui.git#v0.1.1" } }