Fix compose issues
This commit is contained in:
parent
a7d62c1b0f
commit
87bfefd949
12 changed files with 227 additions and 69 deletions
44
.dockerignore
Normal file
44
.dockerignore
Normal 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
1
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
57
README.md
57
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -56,21 +56,27 @@ 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');
|
||||||
$username = config('lemmy.username');
|
|
||||||
$password = config('lemmy.password');
|
if ($cachedToken) {
|
||||||
|
return $cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$username || !$password) {
|
$username = config('lemmy.username');
|
||||||
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials');
|
$password = config('lemmy.password');
|
||||||
}
|
|
||||||
|
|
||||||
$token = $api->login($username, $password);
|
if (!$username || !$password) {
|
||||||
|
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials');
|
||||||
if (!$token) {
|
}
|
||||||
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $token;
|
$token = $api->login($username, $password);
|
||||||
});
|
|
||||||
|
if (!$token) {
|
||||||
|
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::put('lemmy_jwt_token', $token, 3600);
|
||||||
|
|
||||||
|
return $token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -58,29 +58,43 @@ 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');
|
||||||
$username = config('lemmy.username');
|
|
||||||
$password = config('lemmy.password');
|
if ($cachedToken) {
|
||||||
|
return $cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$username || !$password) {
|
$username = config('lemmy.username');
|
||||||
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials');
|
$password = config('lemmy.password');
|
||||||
}
|
|
||||||
|
|
||||||
$token = $this->api->login($username, $password);
|
if (!$username || !$password) {
|
||||||
|
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials');
|
||||||
|
}
|
||||||
|
|
||||||
if (!$token) {
|
$token = $this->api->login($username, $password);
|
||||||
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $token;
|
if (!$token) {
|
||||||
});
|
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::put('lemmy_jwt_token', $token, 3600);
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -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([
|
||||||
|
|
|
||||||
|
|
@ -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
59
docker/.env.production
Normal 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=
|
||||||
|
|
@ -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..."
|
until php -r "
|
||||||
sleep 2
|
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
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue