Fix compose issues

This commit is contained in:
myrmidex 2025-06-30 21:28:15 +02:00
parent a7d62c1b0f
commit 87bfefd949
12 changed files with 227 additions and 69 deletions

44
.dockerignore Normal file
View file

@ -0,0 +1,44 @@
# Development files
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files
.env
.env.*
!.env.production
# Development tools
.git/
.gitignore
README.md
docker-compose.yml
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Laravel development
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/testing/*
storage/framework/views/*
bootstrap/cache/*
# Testing
tests/
phpunit.xml
.phpunit.result.cache
# Build artifacts
vendor/
build/
dist/

1
.gitignore vendored
View file

@ -9,7 +9,6 @@
/vendor /vendor
.env .env
.env.backup .env.backup
.env.production
.phpactor.json .phpactor.json
.phpunit.result.cache .phpunit.result.cache
Homestead.json Homestead.json

View file

@ -35,6 +35,10 @@ COPY . .
# Install dependencies # Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction RUN composer install --no-dev --optimize-autoloader --no-interaction
# Copy production environment file and generate APP_KEY
COPY docker/.env.production .env
RUN php artisan key:generate
# Copy built frontend assets # Copy built frontend assets
COPY --from=frontend-builder /app/public/build /var/www/html/public/build COPY --from=frontend-builder /app/public/build /var/www/html/public/build

View file

@ -23,15 +23,9 @@ ### Docker Compose
ports: ports:
- "8000:8000" - "8000:8000"
environment: environment:
- APP_ENV=production - DB_DATABASE=${DB_DATABASE}
- APP_KEY=${APP_KEY} - DB_USERNAME=${DB_USERNAME}
- DB_CONNECTION=mysql
- DB_HOST=mysql
- DB_PORT=3306
- DB_DATABASE=lemmy_poster
- DB_USERNAME=lemmy_user
- DB_PASSWORD=${DB_PASSWORD} - DB_PASSWORD=${DB_PASSWORD}
- QUEUE_CONNECTION=database
- LEMMY_INSTANCE=${LEMMY_INSTANCE} - LEMMY_INSTANCE=${LEMMY_INSTANCE}
- LEMMY_USERNAME=${LEMMY_USERNAME} - LEMMY_USERNAME=${LEMMY_USERNAME}
- LEMMY_PASSWORD=${LEMMY_PASSWORD} - LEMMY_PASSWORD=${LEMMY_PASSWORD}
@ -46,15 +40,9 @@ ### Docker Compose
image: your-registry/lemmy-poster:latest image: your-registry/lemmy-poster:latest
command: ["queue"] command: ["queue"]
environment: environment:
- APP_ENV=production - DB_DATABASE=${DB_DATABASE}
- APP_KEY=${APP_KEY} - DB_USERNAME=${DB_USERNAME}
- DB_CONNECTION=mysql
- DB_HOST=mysql
- DB_PORT=3306
- DB_DATABASE=lemmy_poster
- DB_USERNAME=lemmy_user
- DB_PASSWORD=${DB_PASSWORD} - DB_PASSWORD=${DB_PASSWORD}
- QUEUE_CONNECTION=database
- LEMMY_INSTANCE=${LEMMY_INSTANCE} - LEMMY_INSTANCE=${LEMMY_INSTANCE}
- LEMMY_USERNAME=${LEMMY_USERNAME} - LEMMY_USERNAME=${LEMMY_USERNAME}
- LEMMY_PASSWORD=${LEMMY_PASSWORD} - LEMMY_PASSWORD=${LEMMY_PASSWORD}
@ -68,10 +56,10 @@ ### Docker Compose
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
environment: environment:
- MYSQL_DATABASE=lemmy_poster - MYSQL_DATABASE=${DB_DATABASE}
- MYSQL_USER=lemmy_user - MYSQL_USER=${DB_USERNAME}
- MYSQL_PASSWORD=${DB_PASSWORD} - MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes: volumes:
- mysql_data:/var/lib/mysql - mysql_data:/var/lib/mysql
restart: unless-stopped restart: unless-stopped
@ -86,25 +74,38 @@ ### Environment Variables
Create a `.env` file with: Create a `.env` file with:
```env ```env
APP_KEY=your-app-key-here # Database Settings
DB_PASSWORD=your-db-password DB_DATABASE=lemmy_poster
DB_ROOT_PASSWORD=your-root-password DB_USERNAME=lemmy_user
DB_PASSWORD=your-password
# Lemmy Settings
LEMMY_INSTANCE=your-lemmy-instance.com LEMMY_INSTANCE=your-lemmy-instance.com
LEMMY_USERNAME=your-lemmy-username LEMMY_USERNAME=your-lemmy-username
LEMMY_PASSWORD=your-lemmy-password LEMMY_PASSWORD=your-lemmy-password
LEMMY_COMMUNITY=your-target-community LEMMY_COMMUNITY=your-target-community
``` ```
Generate the APP_KEY:
```bash
echo "base64:$(openssl rand -base64 32)"
```
### Deployment ### Deployment
1. Build and push the image 1. Build and push the image to your registry
2. Copy the docker-compose.yml to your server 2. Copy the docker-compose.yml to your server
3. Create the .env file with your environment variables 3. Create the .env file with your environment variables
4. Run: `docker-compose up -d` 4. Run: `docker-compose up -d`
The application will automatically:
- Wait for the database to be ready
- Run database migrations on first startup
- Start the queue worker after migrations complete
- Handle race conditions between web and queue containers
The web interface will be available on port 8000, ready for CloudFlare tunnel configuration. The web interface will be available on port 8000, ready for CloudFlare tunnel configuration.
### Architecture
The application uses a multi-container setup:
- **app-web**: Serves the Laravel web interface and handles HTTP requests
- **app-queue**: Processes background jobs (article fetching, Lemmy posting)
- **mysql**: Database storage for articles, logs, and application data
Both app containers use the same Docker image but with different commands (`web` or `queue`). Environment variables are passed from your `.env` file to configure database access and Lemmy integration.

View file

@ -56,7 +56,12 @@ private function syncLemmyChannelPosts(): void
private function getAuthToken(LemmyApiService $api): string private function getAuthToken(LemmyApiService $api): string
{ {
return Cache::remember('lemmy_jwt_token', 3600, function () use ($api) { $cachedToken = Cache::get('lemmy_jwt_token');
if ($cachedToken) {
return $cachedToken;
}
$username = config('lemmy.username'); $username = config('lemmy.username');
$password = config('lemmy.password'); $password = config('lemmy.password');
@ -70,7 +75,8 @@ private function getAuthToken(LemmyApiService $api): string
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed'); throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed');
} }
Cache::put('lemmy_jwt_token', $token, 3600);
return $token; return $token;
});
} }
} }

View file

@ -5,12 +5,8 @@
use App\Events\ExceptionLogged; use App\Events\ExceptionLogged;
use App\Events\ExceptionOccurred; use App\Events\ExceptionOccurred;
use App\Models\Log; use App\Models\Log;
use Illuminate\Contracts\Queue\ShouldQueue; class LogExceptionToDatabase
use Illuminate\Queue\InteractsWithQueue;
class LogExceptionToDatabase implements ShouldQueue
{ {
use InteractsWithQueue;
public function handle(ExceptionOccurred $event): void public function handle(ExceptionOccurred $event): void
{ {

View file

@ -58,7 +58,12 @@ public function publish(Article $article, array $extractedData): ArticlePublicat
private function getAuthToken(): string private function getAuthToken(): string
{ {
return Cache::remember('lemmy_jwt_token', 3600, function () { $cachedToken = Cache::get('lemmy_jwt_token');
if ($cachedToken) {
return $cachedToken;
}
$username = config('lemmy.username'); $username = config('lemmy.username');
$password = config('lemmy.password'); $password = config('lemmy.password');
@ -72,15 +77,24 @@ private function getAuthToken(): string
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed'); throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed');
} }
Cache::put('lemmy_jwt_token', $token, 3600);
return $token; return $token;
});
} }
private function getCommunityId(): int private function getCommunityId(): int
{ {
return Cache::remember("lemmy_community_id_{$this->community}", 3600, function () { $cacheKey = "lemmy_community_id_{$this->community}";
return $this->api->getCommunityId($this->community); $cachedId = Cache::get($cacheKey);
});
if ($cachedId) {
return $cachedId;
}
$communityId = $this->api->getCommunityId($this->community);
Cache::put($cacheKey, $communityId, 3600);
return $communityId;
} }
private function createPublicationRecord(Article $article, array $postData, int $communityId): ArticlePublication private function createPublicationRecord(Article $article, array $postData, int $communityId): ArticlePublication

View file

@ -11,6 +11,21 @@ public static function validate(Article $article): Article
logger('Checking keywords for article: ' . $article->id); logger('Checking keywords for article: ' . $article->id);
$articleData = ArticleFetcher::fetchArticleData($article); $articleData = ArticleFetcher::fetchArticleData($article);
if (!isset($articleData['full_article'])) {
logger()->warning('Article data missing full_article key', [
'article_id' => $article->id,
'url' => $article->url
]);
$article->update([
'is_valid' => false,
'validated_at' => now(),
]);
return $article->refresh();
}
$validationResult = self::validateByKeywords($articleData['full_article']); $validationResult = self::validateByKeywords($articleData['full_article']);
$article->update([ $article->update([

View file

@ -6,11 +6,10 @@ class BelgaHomepageParser
{ {
public static function extractArticleUrls(string $html): array public static function extractArticleUrls(string $html): array
{ {
preg_match_all('/href="https:\/\/www\.belganewsagency\.eu\/([a-z0-9-]+)"/', $html, $matches); preg_match_all('/href="(https:\/\/www\.belganewsagency\.eu\/[a-z0-9-]+)"/', $html, $matches);
$urls = collect($matches[0] ?? []) $urls = collect($matches[1] ?? [])
->unique() ->unique()
->map(fn ($url) => str_replace('href="', '', str_replace('"', '', $url)))
->toArray(); ->toArray();
return $urls; return $urls;

59
docker/.env.production Normal file
View file

@ -0,0 +1,59 @@
APP_NAME="Lemmy Poster"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=lemmy_poster
DB_USERNAME=lemmy_user
DB_PASSWORD=your-password
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
# LEMMY SETTINGS
LEMMY_INSTANCE=
LEMMY_USERNAME=
LEMMY_PASSWORD=
LEMMY_COMMUNITY=

View file

@ -15,14 +15,35 @@ if [ -z "$LEMMY_INSTANCE" ] || [ -z "$LEMMY_USERNAME" ] || [ -z "$LEMMY_PASSWORD
fi fi
# Wait for database to be ready # Wait for database to be ready
until php artisan tinker --execute="DB::connection()->getPdo();" > /dev/null 2>&1; do
echo "Waiting for database connection..." echo "Waiting for database connection..."
sleep 2 until php -r "
try {
\$pdo = new PDO('mysql:host=mysql;port=3306;dbname=' . getenv('DB_DATABASE'), getenv('DB_USERNAME'), getenv('DB_PASSWORD'));
echo 'Connected';
exit(0);
} catch (Exception \$e) {
exit(1);
}
" > /dev/null 2>&1; do
echo "Database not ready, waiting..."
sleep 5
done done
echo "Database connection established."
# Run migrations on first start (web container only) # Run migrations on first start (web container only)
if [ "$1" = "web" ]; then if [ "$1" = "web" ]; then
echo "Running database migrations..."
php artisan migrate --force php artisan migrate --force
elif [ "$1" = "queue" ]; then
echo "Waiting for migrations to complete..."
# Wait for all migrations to actually finish running
until php artisan migrate:status 2>/dev/null | grep -c "Ran" | grep -q "7"; do
echo "Migrations still running, waiting..."
sleep 3
done
echo "All migrations completed."
echo "Waiting for database to stabilize..."
sleep 5
fi fi
# Execute the command based on the argument # Execute the command based on the argument

View file

@ -37,7 +37,7 @@
} else { } else {
logger()->debug('No unpublished valid articles found for Lemmy publishing'); logger()->debug('No unpublished valid articles found for Lemmy publishing');
} }
})->everyFifteenMinutes()->name('publish-to-lemmy'); })->everyFiveMinutes()->name('publish-to-lemmy');
Schedule::call(function () { Schedule::call(function () {
$communityId = config('lemmy.community_id'); $communityId = config('lemmy.community_id');