From b6290c0f8d8992af848db3011a40acb2e5dd826e Mon Sep 17 00:00:00 2001 From: myrmidex Date: Fri, 23 Jan 2026 00:30:05 +0100 Subject: [PATCH] 73 - Fix prod environment --- Dockerfile | 127 ++++++++++++ README.md | 299 ++++++++++----------------- docker/production/Dockerfile | 87 -------- docker/production/docker-compose.yml | 59 +++--- docker/production/nginx.conf | 82 -------- docker/production/start-app.sh | 51 ----- docker/production/supervisord.conf | 45 ---- shell.nix | 31 ++- 8 files changed, 291 insertions(+), 490 deletions(-) create mode 100644 Dockerfile delete mode 100644 docker/production/Dockerfile delete mode 100644 docker/production/nginx.conf delete mode 100644 docker/production/start-app.sh delete mode 100644 docker/production/supervisord.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11ce824 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,127 @@ +# Production Dockerfile with FrankenPHP +FROM dunglas/frankenphp:latest-php8.3-alpine + +# Install system dependencies +RUN apk add --no-cache \ + nodejs \ + npm \ + git \ + mysql-client + +# Install PHP extensions +RUN install-php-extensions \ + pdo_mysql \ + opcache \ + zip \ + gd \ + intl \ + bcmath \ + redis \ + pcntl + +# Install Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /app + +# Set fixed production environment variables +ENV APP_ENV=production \ + APP_DEBUG=false \ + DB_CONNECTION=mysql \ + DB_HOST=db \ + DB_PORT=3306 \ + SESSION_DRIVER=redis \ + CACHE_STORE=redis \ + QUEUE_CONNECTION=redis \ + LOG_CHANNEL=stack \ + LOG_LEVEL=error + +# Copy application code first +COPY . . + +# Install PHP dependencies (production only) +RUN composer install --no-dev --no-interaction --optimize-autoloader + +# Install ALL Node dependencies (including dev for building) +RUN npm ci + +# Build frontend assets +RUN npm run build + +# Remove node_modules after build to save space +RUN rm -rf node_modules + +# Laravel optimizations +RUN php artisan config:cache \ + && php artisan route:cache \ + && php artisan view:cache \ + && composer dump-autoload --optimize + +# Set permissions +RUN chown -R www-data:www-data /app/storage /app/bootstrap/cache + +# Configure Caddy +RUN cat > /etc/caddy/Caddyfile < /start-prod.sh <<'EOF' +#!/bin/sh +set -e + +# Wait for database to be ready +echo "Waiting for database..." +for i in $(seq 1 30); do + if mysqladmin ping -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" --silent 2>/dev/null; then + echo "Database is ready!" + break + fi + echo "Waiting for database... ($i/30)" + sleep 2 +done + +# Run migrations +echo "Running migrations..." +php artisan migrate --force || echo "Migrations failed or already up-to-date" + +# Start Horizon in the background +php artisan horizon & + +# Start FrankenPHP +exec frankenphp run --config /etc/caddy/Caddyfile +EOF + +RUN chmod +x /start-prod.sh + +# Start with our script +CMD ["/start-prod.sh"] diff --git a/README.md b/README.md index 1153f7b..47f8177 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,128 @@ -# Fedi Feed Router (FFR) v1.0.0 +# FFR (Feed to Fediverse Router) -
-FFR Logo +A Laravel-based application for routing RSS/Atom feeds to Fediverse platforms like Lemmy. Built with Laravel, Livewire, and FrankenPHP for a modern, single-container deployment. -**A minimal working version — limited to two hardcoded sources, designed for self-hosters.** -*Future versions will expand configurability and support.* -
+## Features ---- +- **Feed aggregation** - Fetch articles from multiple RSS/Atom feeds +- **Fediverse publishing** - Automatically post to Lemmy communities +- **Route configuration** - Map feeds to specific channels with keywords +- **Approval workflow** - Optional manual approval before publishing +- **Queue processing** - Background job handling with Laravel Horizon +- **Single container deployment** - Simplified hosting with FrankenPHP -## 🔰 Project Overview +## Self-hosting -**One-liner:** FFR routes content from RSS/Atom feeds to the fediverse based on keyword matching. +The production image is available at `codeberg.org/lvl0/ffr:latest`. -FFR is a self-hosted tool that monitors RSS/Atom feeds, filters articles based on keywords, and automatically publishes matching content to fediverse platforms like Lemmy. This v1.0.0 release provides a working foundation with two hardcoded news sources (CBC and BBC), designed specifically for self-hosters who want a simple, privacy-first solution without SaaS dependencies. +### docker-compose.yml -## âš™ī¸ Features +```yaml +services: + app: + image: codeberg.org/lvl0/ffr:latest + container_name: ffr_app + restart: always + ports: + - "8000:8000" + environment: + APP_KEY: "${APP_KEY}" + APP_URL: "${APP_URL}" + DB_DATABASE: "${DB_DATABASE}" + DB_USERNAME: "${DB_USERNAME}" + DB_PASSWORD: "${DB_PASSWORD}" + REDIS_HOST: redis + REDIS_PORT: 6379 + volumes: + - app_storage:/app/storage + depends_on: + - db + - redis + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/up"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s -Current v1.0.0 features: -- ✅ Fetches articles from two hardcoded RSS feeds (CBC News, BBC News) -- ✅ Keyword-based content filtering and matching -- ✅ Automatic posting to Lemmy communities -- ✅ Web dashboard for monitoring and management -- ✅ Docker-based deployment for easy self-hosting -- ✅ Privacy-first design with no external dependencies + db: + image: mariadb:11 + container_name: ffr_db + restart: always + environment: + MYSQL_DATABASE: "${DB_DATABASE}" + MYSQL_USER: "${DB_USERNAME}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" + volumes: + - db_data:/var/lib/mysql -Limitations (to be addressed in future versions): -- Feed sources are currently hardcoded (not user-configurable) -- Only supports Lemmy as target platform -- Basic keyword matching (no regex or complex rules yet) + redis: + image: redis:7-alpine + container_name: ffr_redis + restart: always + volumes: + - redis_data:/data -## 🚀 Installation - -### Quick Start with Docker - -1. **Clone the repository:** - ```bash - git clone https://codeberg.org/lvl0/ffr.git - cd ffr - ``` - -2. **Create environment file:** - ```bash - cp docker/production/.env.example .env - ``` - -3. **Configure your environment variables:** - ```env - # Required variables only - APP_URL=http://your-domain.com:8000 - DB_PASSWORD=your-secure-db-password - DB_ROOT_PASSWORD=your-secure-root-password - ``` - -4. **Start the application:** - ```bash - docker-compose -f docker/production/docker-compose.yml up -d - ``` - -The application will be available at `http://localhost:8000` - -### System Requirements - -- Docker and Docker Compose (or Podman) -- 2GB RAM minimum -- 10GB disk space -- Linux/macOS/Windows with WSL2 - -## đŸ•šī¸ Usage - -### Web Interface - -Access the dashboard at `http://localhost:8000` to: -- View fetched articles -- Monitor posting queue -- Check system logs -- Manage keywords (coming in v2.0) - -### Manual Commands - -Trigger article refresh manually: -```bash -docker compose exec app php artisan article:refresh +volumes: + db_data: + redis_data: + app_storage: ``` -View application logs: -```bash -docker compose logs -f app -``` +### Environment Variables -### Scheduled Tasks +| Variable | Required | Description | +|----------|----------|-------------| +| `APP_KEY` | Yes | Encryption key. Generate with: `echo "base64:$(openssl rand -base64 32)"` | +| `APP_URL` | Yes | Your domain (e.g., `https://ffr.example.com`) | +| `DB_DATABASE` | Yes | Database name | +| `DB_USERNAME` | Yes | Database user | +| `DB_PASSWORD` | Yes | Database password | +| `DB_ROOT_PASSWORD` | Yes | MariaDB root password | -The application automatically: -- Fetches new articles every hour -- Publishes matching articles every 5 minutes -- Syncs with Lemmy communities every 10 minutes +## Development -## 📜 Logging & Debugging - -**Log locations:** -- Application logs: Available in web dashboard under "Logs" section -- Docker logs: `docker compose logs -f app` -- Laravel logs: Inside container at `/var/www/html/backend/storage/logs/` - -**Debug mode:** -To enable debug mode for troubleshooting, add to your `.env`: -```env -APP_DEBUG=true -``` -âš ī¸ Remember to disable debug mode in production! - -## 🤝 Contributing - -We welcome contributions! Here's how you can help: - -1. **Report bugs:** Open an issue describing the problem -2. **Suggest features:** Create an issue with your idea -3. **Submit PRs:** Fork, create a feature branch, and submit a pull request -4. **Improve docs:** Documentation improvements are always appreciated - -For development setup, see the [Development Setup](#development-setup) section below. - -## 📘 License - -This project is licensed under the GNU Affero General Public License v3.0 (AGPLv3). -See [LICENSE](LICENSE) file for details. - -## 🧭 Roadmap - -### v1.0.0 (Current Release) -- ✅ Basic feed fetching from hardcoded sources -- ✅ Keyword filtering -- ✅ Lemmy posting -- ✅ Web dashboard -- ✅ Docker deployment - -### v2.0.0 (Planned) -- [ ] User-configurable feed sources -- [ ] Advanced filtering rules (regex, boolean logic) -- [ ] Support for Mastodon and other ActivityPub platforms -- [ ] API for external integrations -- [ ] Multi-user support with permissions - -### v3.0.0 (Future) -- [ ] Machine learning-based content categorization -- [ ] Feed discovery and recommendations -- [ ] Scheduled posting with optimal timing -- [ ] Analytics and insights dashboard - ---- - -## Development Setup - -For contributors and developers who want to work on FFR: - -### Prerequisites - -- Podman and podman-compose (or Docker) -- Git -- PHP 8.2+ (for local development) -- Node.js 18+ (for frontend development) - -### Quick Start - -1. **Clone and start the development environment:** - ```bash - git clone https://codeberg.org/lvl0/ffr.git - cd ffr - ./docker/dev/podman/start-dev.sh - ``` - -2. **Access the development environment:** - - Web interface: http://localhost:8000 - - Vite dev server: http://localhost:5173 - - Database: localhost:3307 - - Redis: localhost:6380 - -### Development Commands +### NixOS / Nix ```bash -# Run tests with coverage -podman-compose -f docker/dev/podman/docker-compose.yml exec app bash -c "cd backend && XDEBUG_MODE=coverage php artisan test --coverage-html=coverage-report" - -# Execute artisan commands -podman-compose -f docker/dev/podman/docker-compose.yml exec app php artisan migrate -podman-compose -f docker/dev/podman/docker-compose.yml exec app php artisan tinker - -# View logs -podman-compose -f docker/dev/podman/docker-compose.yml logs -f - -# Access container shell -podman-compose -f docker/dev/podman/docker-compose.yml exec app bash - -# Stop environment -podman-compose -f docker/dev/podman/docker-compose.yml down +git clone https://codeberg.org/lvl0/ffr.git +cd ffr +nix-shell ``` -### Development Features +The shell will display available commands and optionally start the containers for you. -- **Hot reload:** Vite automatically reloads frontend changes -- **Pre-seeded database:** Sample data for immediate testing -- **Laravel Horizon:** Queue monitoring dashboard -- **Xdebug:** Configured for debugging and code coverage -- **Redis:** For caching, sessions, and queues +#### Available Commands ---- +| Command | Description | +|---------|-------------| +| `dev-up` | Start development environment | +| `dev-down` | Stop development environment | +| `dev-restart` | Restart containers | +| `dev-logs` | Follow app logs | +| `dev-logs-db` | Follow database logs | +| `dev-shell` | Enter app container | +| `dev-artisan ` | Run artisan commands | +| `prod-build [tag]` | Build and push prod image (default: latest) | + +#### Services + +| Service | URL | +|---------|-----| +| App | http://localhost:8000 | +| Vite | http://localhost:5173 | +| MariaDB | localhost:3307 | +| Redis | localhost:6380 | + +### Other Platforms + +Contributions welcome for development setup instructions on other platforms. + +## License + +This project is open-source software licensed under the [AGPL-3.0 license](LICENSE). ## Support -For help and support: -- đŸ’Ŧ Open a [Discussion](https://codeberg.org/lvl0/ffr/discussions) -- 🐛 Report [Issues](https://codeberg.org/lvl0/ffr/issues) - ---- - -
-Built with â¤ī¸ for the self-hosting community -
\ No newline at end of file +For issues and questions, please use [Codeberg Issues](https://codeberg.org/lvl0/ffr/issues). diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile deleted file mode 100644 index 8690f35..0000000 --- a/docker/production/Dockerfile +++ /dev/null @@ -1,87 +0,0 @@ -# Multi-stage build for FFR Laravel application -FROM node:22-alpine AS frontend-builder - -WORKDIR /app - -# Copy frontend package files -COPY frontend/package*.json ./ - -# Install Node dependencies -RUN npm ci - -# Copy frontend source -COPY frontend/ ./ - -# Build frontend assets -RUN npm run build - -# PHP runtime stage -FROM php:8.4-fpm-alpine - -# Install system dependencies -RUN apk add --no-cache \ - git \ - curl \ - libpng-dev \ - libxml2-dev \ - zip \ - unzip \ - oniguruma-dev \ - mysql-client \ - nginx \ - supervisor \ - autoconf \ - gcc \ - g++ \ - make - -# Install PHP extensions -RUN docker-php-ext-install \ - pdo_mysql \ - mbstring \ - exif \ - pcntl \ - bcmath \ - gd \ - && pecl install redis \ - && docker-php-ext-enable redis - -# Install Composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -# Set working directory -WORKDIR /var/www/html - -# Copy application code -COPY . . - -# Install PHP dependencies in backend directory -WORKDIR /var/www/html/backend -RUN composer install --no-dev --optimize-autoloader --no-interaction - -# Copy built frontend assets from builder stage to frontend dist -COPY --from=frontend-builder /app/dist/ /var/www/html/frontend/dist/ - -# Back to main directory -WORKDIR /var/www/html - -# Copy nginx and supervisor configurations -COPY docker/production/nginx.conf /etc/nginx/http.d/default.conf -COPY docker/production/supervisord.conf /etc/supervisord.conf -COPY docker/production/start-app.sh /usr/local/bin/start-app - -# Set proper permissions -RUN chown -R www-data:www-data /var/www/html \ - && chmod -R 755 /var/www/html/backend/storage \ - && chmod -R 755 /var/www/html/backend/bootstrap/cache \ - && chmod +x /usr/local/bin/start-app - -# Expose port 80 for nginx -EXPOSE 80 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD cd /var/www/html/backend && php artisan --version || exit 1 - -# Start the application -CMD ["/usr/local/bin/start-app"] \ No newline at end of file diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml index 8b9aa31..20ce7c5 100644 --- a/docker/production/docker-compose.yml +++ b/docker/production/docker-compose.yml @@ -1,23 +1,28 @@ +# =================== +# FFR Production Services +# =================== + services: app: - image: codeberg.org/lvl0/ffr:v1.0.0-rc1 - container_name: ffr-app + build: + context: ../.. + dockerfile: Dockerfile + image: codeberg.org/lvl0/ffr:latest + container_name: ffr_app restart: unless-stopped - environment: - - APP_URL=${APP_URL} - - DB_CONNECTION=mysql - - DB_HOST=db - - DB_PORT=3306 - - DB_DATABASE=ffr - - DB_USERNAME=ffr_user - - DB_PASSWORD=${DB_PASSWORD} - - REDIS_HOST=redis - - REDIS_PORT=6379 - - CACHE_DRIVER=redis - - SESSION_DRIVER=redis - - QUEUE_CONNECTION=redis ports: - - "8000:80" + - "8000:8000" + environment: + APP_NAME: "${APP_NAME:-FFR}" + APP_KEY: "${APP_KEY}" + APP_URL: "${APP_URL}" + DB_HOST: db + DB_PORT: 3306 + DB_DATABASE: "${DB_DATABASE:-ffr}" + DB_USERNAME: "${DB_USERNAME:-ffr}" + DB_PASSWORD: "${DB_PASSWORD}" + REDIS_HOST: redis + REDIS_PORT: 6379 depends_on: db: condition: service_healthy @@ -27,28 +32,28 @@ services: - ffr-network db: - image: docker.io/library/mysql:8.4 - container_name: ffr-db + image: mariadb:11 + container_name: ffr_db restart: unless-stopped environment: - - MYSQL_DATABASE=ffr - - MYSQL_USER=ffr_user - - MYSQL_PASSWORD=${DB_PASSWORD} - - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + MYSQL_DATABASE: "${DB_DATABASE:-ffr}" + MYSQL_USER: "${DB_USERNAME:-ffr}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" volumes: - db_data:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ffr_user", "-p${DB_PASSWORD}"] + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s timeout: 5s retries: 5 - interval: 3s start_period: 30s networks: - ffr-network redis: - image: docker.io/library/redis:7-alpine - container_name: ffr-redis + image: redis:7-alpine + container_name: ffr_redis restart: unless-stopped volumes: - redis_data:/data @@ -61,6 +66,4 @@ networks: volumes: db_data: - driver: local redis_data: - driver: local diff --git a/docker/production/nginx.conf b/docker/production/nginx.conf deleted file mode 100644 index 5ff7994..0000000 --- a/docker/production/nginx.conf +++ /dev/null @@ -1,82 +0,0 @@ -server { - listen 80; - server_name localhost; - - # Serve static React build files - root /var/www/html/frontend/dist; - index index.html; - - # API requests to Laravel backend - location /api/ { - root /var/www/html/backend/public; - try_files /index.php =404; - - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /var/www/html/backend/public/index.php; - include fastcgi_params; - - # Increase timeouts for long-running requests - fastcgi_read_timeout 300; - fastcgi_send_timeout 300; - } - - # Serve Laravel public assets (images, etc.) - location /images/ { - alias /var/www/html/backend/public/images/; - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # React app - catch all routes - location / { - try_files $uri $uri/ /index.html; - } - - # Static assets with far-future expiry - location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - # Security headers - add_header X-Frame-Options "SAMEORIGIN"; - add_header X-Content-Type-Options "nosniff"; - add_header X-XSS-Protection "1; mode=block"; - - # Deny access to hidden files - location ~ /\.ht { - deny all; - } - - # Deny access to sensitive files - location ~ /\.(env|git) { - deny all; - } - - location = /favicon.ico { - access_log off; - log_not_found off; - } - - location = /robots.txt { - access_log off; - log_not_found off; - } - - # Gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_proxied any; - gzip_comp_level 6; - gzip_types - text/plain - text/css - text/xml - text/javascript - application/javascript - application/xml+rss - application/json; -} \ No newline at end of file diff --git a/docker/production/start-app.sh b/docker/production/start-app.sh deleted file mode 100644 index 0d0df99..0000000 --- a/docker/production/start-app.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh - -# Create .env file if it doesn't exist -if [ ! -f /var/www/html/backend/.env ]; then - cp /var/www/html/backend/.env.example /var/www/html/backend/.env 2>/dev/null || touch /var/www/html/backend/.env -fi - -# Wait for database to be ready using PHP -echo "Waiting for database..." -until php -r " -\$host = getenv('DB_HOST') ?: 'db'; -\$user = getenv('DB_USERNAME') ?: 'ffr_user'; -\$pass = getenv('DB_PASSWORD'); -\$db = getenv('DB_DATABASE') ?: 'ffr'; -try { - \$pdo = new PDO(\"mysql:host=\$host;dbname=\$db\", \$user, \$pass, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false - ]); - echo 'Database ready'; - exit(0); -} catch (Exception \$e) { - exit(1); -} -" 2>/dev/null; do - echo "Database not ready, waiting..." - sleep 1 -done -echo "Database connection established!" - -# Generate app key if not set -if ! grep -q "APP_KEY=base64:" /var/www/html/backend/.env; then - cd /var/www/html/backend && php artisan key:generate --force -fi - -# Laravel optimizations for production -cd /var/www/html/backend -php artisan config:cache -php artisan route:cache -php artisan view:cache - -# Run migrations -cd /var/www/html/backend -php artisan migrate --force - -# Run all seeders (same as dev) -cd /var/www/html/backend -php artisan db:seed --force - -# Start supervisor to manage nginx and php-fpm -supervisord -c /etc/supervisord.conf \ No newline at end of file diff --git a/docker/production/supervisord.conf b/docker/production/supervisord.conf deleted file mode 100644 index 5df63d5..0000000 --- a/docker/production/supervisord.conf +++ /dev/null @@ -1,45 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:nginx] -command=nginx -g "daemon off;" -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=10 - -[program:php-fpm] -command=php-fpm --nodaemonize -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=10 - -[program:horizon] -command=php /var/www/html/backend/artisan horizon -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=20 - -[program:scheduler] -command=sh -c "while true; do php /var/www/html/backend/artisan schedule:run --verbose --no-interaction; sleep 60; done" -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -priority=20 \ No newline at end of file diff --git a/shell.nix b/shell.nix index 20e63fc..e14efc4 100644 --- a/shell.nix +++ b/shell.nix @@ -3,8 +3,8 @@ pkgs.mkShell { buildInputs = with pkgs; [ # PHP and tools - php84 - php84Packages.composer + php83 + php83Packages.composer # Node.js and npm nodejs_22 @@ -81,6 +81,32 @@ pkgs.mkShell { podman-compose -f $COMPOSE_FILE exec app php artisan "$@" } + # =================== + # PROD COMMANDS + # =================== + prod-build() { + local tag="''${1:-latest}" + local image="codeberg.org/lvl0/ffr:$tag" + + echo "Building production image: $image" + if ! podman build -t "$image" -f Dockerfile .; then + echo "Build failed!" + return 1 + fi + + echo "" + echo "Pushing to registry..." + if ! podman push "$image"; then + echo "" + echo "Push failed! You may need to login first:" + echo " podman login codeberg.org" + return 1 + fi + + echo "" + echo "Done! Image pushed: $image" + } + # =================== # WELCOME MESSAGE # =================== @@ -99,6 +125,7 @@ pkgs.mkShell { echo " dev-logs-db Tail database logs" echo " dev-shell Shell into app container" echo " dev-artisan Run artisan command" + echo " prod-build [tag] Build and push prod image (default: latest)" echo "" echo "Services:" echo " app Laravel + Vite http://localhost:8000"