A self-hosted PHP application that automatically posts AI-generated images to Instagram Feed and Stories. Designed for hands-free operation via cron jobs, with a real-time dashboard for monitoring.
- Features
- Architecture
- Requirements
- Installation
- cPanel Hosting Setup
- Configuration
- Cron Setup
- Dashboard
- Image Processing Pipeline
- Security
- Troubleshooting
- Project Structure
- License
- Automated Posting β Runs via cron, no manual intervention needed
- AI-Generated Captions β Uses OpenRouter (Gemma 3 Vision) to analyze each image and write poetic captions
- Dual Posting β Publishes to both Instagram Feed and Stories simultaneously
- Aspect Ratio Correction β Automatically adds black bars to images outside Instagram's ratio requirements (4:5 to 1.91:1)
- Format Conversion β Converts PNG/WebP to JPEG for Instagram compatibility
- Error Recovery β Recovers pending stories if publishing fails mid-process
- Real-Time Dashboard β Monitor posts, errors, logs, and trigger manual runs
- Atomic Publishing β Feed and Story are published together; if Feed fails, Story is skipped
- File-Based Storage β No database required; everything runs on flat files
- Mutex Locking β Prevents concurrent poster runs
βββββββββββββββ ββββββββββββββββ βββββββββββββββββββ
β Cron Job ββββββΆβ cron.php ββββββΆβ App.php β
β (hourly) β β (CLI entry) β β (Orchestrator) β
βββββββββββββββ ββββββββββββββββ ββββββββββ¬βββββββββ
β
βββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
β β β
βββββββΌβββββββ βββββββββΌβββββββ βββββββββΌβββββββ
β ImageQueue β β GroqAI β β Instagram β
β (scanner) β β (OpenRouter)β β (Graph API) β
ββββββββββββββ ββββββββββββββββ ββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Dashboard (Web UI) β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββ β
β β Stats β β Posts β β Errors β β Log β β Manual Run β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The posting flow is a single CLI process with no HTTP intermediaries:
- Cron triggers
cron.phpdirectly via CLI - App acquires a mutex lock to prevent concurrent runs
- ImageQueue selects a random valid image from the
images/directory - ImageProcessor fixes aspect ratio and converts to JPEG
- GroqAI (via OpenRouter) generates a caption using the Gemma 3 vision model
- Instagram creates Feed and Story containers, waits for processing, then publishes
- Posted images are deleted (configurable) and results are logged
- PHP 8.1+ with the following extensions:
gd(image processing)curl(API requests)json(data handling)mbstring(string handling)
- Web Server: Apache or Nginx (for dashboard access)
- Instagram Business Account connected to a Facebook Page
- Meta Developer App with
instagram_basicandinstagram_content_publishpermissions - OpenRouter API Key (free tier available at openrouter.ai)
- Cron access (for scheduled posting)
git clone https://github.com/rodrigocaetanooficial/PHPInstagramAutoPoster.git
cd PHPInstagramAutoPostercp config-sample.php config.php
nano config.phpFill in your API credentials (see Configuration below).
chmod 755 images/
chmod 777 logs/The logs/ directory must be writable by the web server user and the cron user.
Place your images in the images/ directory. Supported formats: JPG, JPEG, PNG, WebP. Minimum size: 400Γ400px.
crontab -eAdd this line to run every hour:
0 * * * * /usr/bin/php /path/to/cron.php >> /path/to/logs/cron.log 2>&1Open https://yourdomain.com in your browser. Default credentials:
- Username:
admin - Password: (set your own in
config.php)
This section covers everything you need to deploy on shared hosting with cPanel (e.g., HostGator, Hostinger, ViawWeb, etc.).
- In cPanel β File Manager, navigate to
public_html/(or a subdomain folder) - Upload all project files, except
config.php(which contains secrets) - Create
config.phpmanually in cPanel's File Manager using the contents ofconfig-sample.php
Alternatively, use FTP (FileZilla, etc.) to upload all files at once.
In cPanel β File Manager, right-click each folder and choose "Change Permissions":
| Folder | Permission |
|---|---|
images/ |
755 |
images/_stories/ |
755 |
logs/ |
777 |
Or via cPanel β Terminal (if available):
chmod 755 images/ images/_stories/
chmod 777 logs/- Go to cPanel β MultiPHP Manager (or PHP Selector)
- Set PHP version to 8.1 or higher for your domain
- Go to cPanel β PHP Extensions (or Select PHP Extensions) and ensure these are enabled:
- β
gd - β
curl - β
json - β
mbstring
- β
- In cPanel β Cron Jobs
- Set "Common Settings" to
Once Per Hour(or custom:0 * * * *) - In the Command field, enter:
/usr/local/bin/php /home/YOUR_CPANEL_USER/public_html/cron.php >> /home/YOUR_CPANEL_USER/public_html/logs/cron.log 2>&1
β οΈ ReplaceYOUR_CPANEL_USERwith your actual cPanel username (visible in the top-right of cPanel). The PHP path may vary β try/usr/bin/phpor/usr/local/php81/bin/phpif the above doesn't work.
To find the correct PHP path via cPanel Terminal:
which phpFill in all values in config.php on the server. Key fields:
define('IMAGES_DIR', '/home/YOUR_CPANEL_USER/public_html/images');
define('IMAGES_URL', 'https://yourdomain.com/images');
define('LOGS_DIR', '/home/YOUR_CPANEL_USER/public_html/logs');
β οΈ Use absolute server paths forIMAGES_DIRandLOGS_DIR. You can find the full path in cPanel β File Manager (shown in the address bar or via Terminal:pwd).
The included .htaccess already blocks direct access to config.php, src/, and logs/. Verify it is uploaded and that Apache's mod_rewrite is enabled (it is on most cPanel hosts by default).
To confirm protection is working, open https://yourdomain.com/config.php in a browser β you should receive a 403 Forbidden error.
In cPanel β Terminal (or via SSH):
php /home/YOUR_CPANEL_USER/public_html/cron.phpThen check the log:
tail -f /home/YOUR_CPANEL_USER/public_html/logs/debug.logA successful run will end with: === Poster run completed successfully ===
All settings are in config.php. Here's what each section does:
| Constant | Description |
|---|---|
FB_PAGE_ACCESS_TOKEN |
System User Access Token (permanent) or Long-Lived Page Access Token |
IG_BUSINESS_ID |
Instagram Business Account ID |
GRAPH_API_VERSION |
Graph API version (default: v22.0) |
Recommended: System User Token (never expires)
- In Meta Business Suite β Business Settings β System Users
- Create a System User and assign it to your Page and Instagram account
- Generate a token with
instagram_basicandinstagram_content_publishpermissions - This token does not expire β ideal for automated posting
Alternative: Long-lived Page Access Token (expires in ~60 days)
- Go to developers.facebook.com/tools/explorer
- Select your App and Page, add
instagram_basic+instagram_content_publishpermissions - Generate and exchange for a long-lived token
| Constant | Description |
|---|---|
GROQ_API_KEY |
Your API key from openrouter.ai |
GROQ_MODEL |
Vision model (default: google/gemma-3-4b-it:free) |
GROQ_API_URL |
OpenRouter endpoint (default: https://openrouter.ai/api/v1/chat/completions) |
Note: Despite the
GROQ_prefix (kept for backwards compatibility), this now uses the OpenRouter API with Google's Gemma 3 vision model. Get a free key at openrouter.ai.
| Constant | Description |
|---|---|
IMAGES_DIR |
Absolute server path to the images directory |
IMAGES_URL |
Public HTTPS URL where images are accessible (must be reachable by Instagram's servers) |
LOGS_DIR |
Absolute server path to the logs directory |
| Constant | Default | Description |
|---|---|---|
DELETE_POSTED_IMAGE |
true |
Delete original image after posting |
MAX_LOG_ENTRIES |
100 |
Maximum entries in JSON log files |
CONTAINER_RETRY_COUNT |
3 |
Retries for container creation |
CONTAINER_RETRY_DELAY |
5 |
Seconds between retries |
STORY_PUBLISH_DELAY |
15 |
Seconds to wait before publishing Story |
CONTAINER_MAX_WAIT |
90 |
Max seconds to wait for container processing |
| Constant | Default | Description |
|---|---|---|
ALLOWED_EXTENSIONS |
jpg, jpeg, png, webp |
Accepted file extensions |
FEED_MIN_RATIO |
0.8 |
Minimum aspect ratio (4:5 portrait) |
FEED_MAX_RATIO |
1.91 |
Maximum aspect ratio (1.91:1 landscape) |
MIN_IMAGE_SIZE |
400 |
Minimum width/height in pixels |
| Constant | Description |
|---|---|
DASHBOARD_USER |
Dashboard username |
DASHBOARD_PASSWORD_HASH |
Bcrypt hash of the password |
Generate a password hash:
php -r "echo password_hash('your_password', PASSWORD_BCRYPT);"0 * * * * /usr/bin/php /var/www/html/cron.php >> /var/www/html/logs/cron.log 2>&1In cPanel β Cron Jobs, set frequency to 0 * * * * and command to:
/usr/local/bin/php /home/YOUR_CPANEL_USER/public_html/cron.php >> /home/YOUR_CPANEL_USER/public_html/logs/cron.log 2>&1php cron.phptail -n 50 logs/debug.log
tail -n 10 logs/posts.json
tail -n 10 logs/errors.jsonThe dashboard provides real-time monitoring:
| Section | Description |
|---|---|
| Stats Cards | Image count, total posts, errors (24h), next cron run |
| Manual Run | Trigger a post immediately with CSRF protection |
| Run Log | Live debug log showing every step of the posting process |
| Recent Posts | Last 10 posts with timestamps, captions, and post IDs |
| Recent Errors | Last 10 errors with context for debugging |
| Trigger Log | Diagnostic log for manual run attempts |
| Button | Purpose |
|---|---|
| π Clear Lock | Removes stale mutex lock file |
| π Check Lock | Shows current lock status and age |
| π§ͺ Test Lock | Tests filesystem permissions for fopen/flock |
Original Image
β
βΌ
βββββββββββββββββββββββ
β Aspect Ratio Fix β Adds black bars if ratio is outside 0.8β1.91
β (ImageProcessor) β Too tall β pillarbox | Too wide β letterbox
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β AI Caption Gen β Sends image URL to OpenRouter (Gemma 3 vision)
β (GroqAI class) β Returns poetic caption (max 150 chars)
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Convert to JPEG β Converts PNG/WebP to 92% quality JPEG
β (ImageProcessor) β Sanitizes filename for URL safety
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Story Image β Creates 1080Γ1920 story with letterboxing
β (ImageProcessor) β Saves to images/_stories/
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Instagram API β 1. Create Feed container β wait β publish
β (Instagram) β 2. Create Story container β wait β publish
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Cleanup β Deletes posted images (if configured)
β β Logs result to posts.json
βββββββββββββββββββββββ
| Feature | Implementation |
|---|---|
| Password Hashing | Bcrypt via password_verify() |
| CSRF Protection | Session-based tokens with hash_equals() |
| Rate Limiting | IP-based login throttling (5 attempts, 15min lockout) |
| Session Security | HttpOnly cookies, SameSite=Strict, strict mode |
| Input Sanitization | htmlspecialchars() on all output |
| Filename Sanitization | Strips URL-unsafe characters from filenames |
| Access Control | .htaccess blocks direct access to config, src, logs |
| Security Headers | X-Content-Type-Options, X-Frame-Options, CSP, etc. |
- Never commit
config.phpβ it contains API keys and passwords - Keep
IMAGES_URLpublic β Instagram's servers must be able to fetch images - Use HTTPS β required for session cookies and API security
The mutex lock file is stale. Clear it:
rm /path/to/logs/poster.lockOr use the π Clear Lock button in the dashboard.
chmod -R 777 /path/to/logs/On cPanel, do this via File Manager β Change Permissions.
- Check
logs/debug.logfor error details - Verify
IMAGES_URLis publicly accessible (Instagram must fetch it via HTTPS) - Ensure
FB_PAGE_ACCESS_TOKENis valid and not expired - Check
logs/errors.jsonfor API errors
This happens with short-lived tokens. Solutions:
- Best: Use a System User Token (permanent, never expires) β see Configuration
- Alternative: Regenerate a long-lived token from the Graph API Explorer and update
FB_PAGE_ACCESS_TOKENinconfig.php
The posting process takes 2β3 minutes. Increase your server timeouts:
On cPanel β Add to .htaccess:
php_value max_execution_time 300Nginx (add to your site config):
proxy_read_timeout 300s;Apache (apache2.conf):
Timeout 300PHP-FPM (pool config):
request_terminate_timeout = 300- Verify the PHP path: in cPanel β Terminal, run
which php - Check that cron output is being written: look in
logs/cron.log - Ensure
IMAGES_DIRandLOGS_DIRuse absolute paths inconfig.php
βββ config-sample.php # Configuration template (safe to commit)
βββ config.php # Your configuration (NEVER commit)
βββ cron.php # CLI entry point for cron jobs
βββ index.php # Dashboard entry point
βββ login.php # Login page
βββ logout.php # Logout handler
βββ api.php # JSON API for dashboard AJAX calls
βββ .htaccess # Apache security rules
β
βββ assets/
β βββ app.js # Dashboard JavaScript
β βββ style.css # Dashboard CSS (Neon 80s dark theme)
β
βββ images/ # Drop your images here
β βββ _stories/ # Auto-generated story images
β
βββ logs/
β βββ debug.log # Plain text debug log
β βββ posts.json # Structured post history
β βββ errors.json # Structured error history
β βββ trigger.log # Manual run diagnostics
β βββ poster.lock # Mutex lock file
β
βββ src/
βββ App.php # Main orchestrator
βββ Auth.php # Authentication, CSRF, rate limiting
βββ GroqAI.php # AI caption generation via OpenRouter API
βββ ImageProcessor.php # GD image manipulation
βββ ImageQueue.php # Image scanning and selection
βββ Instagram.php # Instagram Graph API client
βββ Logger.php # File-based logging
MIT License. See LICENSE for details.