RealEZState Instagram Autopilot
RealEZState is a real estate investor education brand in Bradenton, Florida. It needs to show up on Instagram every day, because that is how organic reach works. Someone used to do that by hand. Now nobody does. The account posts twice a day, seven days a week, and every post is written, designed, scheduled and published with no person involved.
It runs on hardware the business already owns and costs about $2.40 a month, which is the OpenAI bill. Tools normally used for this job cost $30 to $100 a month per brand. I built it in June 2026 in about three days.
What the system actually does
Twice a day, at 09:00 and 16:00 Eastern, a timer fires and the system picks a subject. A topic queued by a human in a shared SharePoint list wins. Otherwise it flips a coin: half the time it draws from a rotation of twelve topics, skipping any used in the last fourteen days, half the time it runs in a free creativity mode guided only by brand voice.
It writes the post, renders a branded graphic with a headline, subheading and three checkmark bullets, publishes that image to a public web address, schedules it, records the topic, and writes an audit line back to SharePoint. About five minutes end to end. The brand’s static marketing site at realezestate.com sits on the same infrastructure, but the pipeline is the project.
Why I did not just buy a scheduler
Hootsuite, Buffer and Later solve the calendar problem and stop there. They do not write the content or render the artwork, and they charge per brand per month forever. Going direct to the Instagram Graph API means OAuth2, long-lived token refresh, image containers and rate limits. You do that once and own it.
Results
| Measure | Result |
|---|---|
| Autonomous cadence | 2 posts per day, 7 days a week, 09:00 and 16:00 America/New_York |
| Human intervention required | Zero for daily operations |
| Recurring cost | About $2.40/month (OpenAI API only) |
| Comparable SaaS cost avoided | $30-100/month |
| Time from concept to production | About 3 days (25-30 focused build hours) |
| End-to-end pipeline latency | About 5 minutes from trigger to published |
| Platform extensibility | 20+ social platforms supported out of the box |
| Content generation success rate | 100% across 6 validation runs |
On that last row, six runs is a small sample and I would rather say so. Each of the six produced a valid, rendered, successfully scheduled post. That is all it means.
The n8n pivot, which is the part worth reading
The original build used n8n for scheduling. I spent roughly eighteen hours configuring and testing three interconnected workflows. They showed as active. The autonomous triggers never fired.
I stopped guessing and opened the n8n SQLite database directly. The workflow rows had active=1 as expected, but activeVersionId was NULL, and the scheduler was silently refusing to run anything with a null active version. Nothing in the interface showed it. This is a documented bug in n8n v2.8.4 with no known workaround on that version: version-bound, not a design flaw, but a hard blocker.
So I discarded the scheduling layer and replaced it with Postiz, a self-hosted open-source scheduler backed by Temporal. The image generator, brand assets, credentials, Cloudflare Tunnel routes and SharePoint integration survived untouched, because the system was built as separate services rather than one workflow. Eighteen hours of configuration was lost. The infrastructure was not.
Architecture
A Proxmox VE host runs seven LXC containers, one service per container for failure isolation:
- the local model container Ollama serving llama3.1:8b
- the legacy workflow container legacy n8n, kept for reference
- the web container nginx serving the realezestate.com static site
- the application container the image generator (Python/Flask) and the autopost orchestrator
- the scheduling container Postiz, a nine-container Docker Compose stack with Postgres 17, Redis 7.2, Temporal 1.28 and Elasticsearch
Public HTTPS runs through Cloudflare Tunnels: no public IP, no port forwarding, no self-managed TLS. Two hostnames are exposed, one for image delivery and one for the scheduler dashboard and API, for the Postiz dashboard and API.
Image generator
A Flask REST API with API-token auth on an X-API-Token header. /api/ig-images/generate renders a branded image from a title, subtitle, three checkmark-boxed bullets and a theme of blue, orange or navy. /approve stages to pending, /promote publishes to the nginx public directory, /archive retires them.
Orchestrator and the bridge endpoint
daily-autopost.py runs as a one-shot systemd service fired by a timer. It queries SharePoint through the Microsoft Graph API for the oldest row with status queued and uses it as a manual override. Otherwise it runs the topic split above, then calls GPT-4o-mini with a strict JSON schema requiring title, subtitle, exactly three bullets of 4 to 38 characters, caption, theme enum, hashtags and topic slug.
/publish-via-postiz is the bridge. It confirms the image is in the pending directory, promotes it to public (idempotent, so it succeeds if already there), uploads the public URL through POST /public/v1/upload-from-url, then creates the post through POST /public/v1/posts with type=schedule, Instagram settings, integration ID and caption. Failures degrade: an upload failure keeps the promoted image for retry, a scheduling failure returns a structured error naming the file to clean up.
DevOps patterns
- Credentials in mode-600
/etc/*.envfiles injected through systemdEnvironmentFile, never in unit files or repository code - logrotate, weekly, four weeks of compressed retention
- Timer plus oneshot service instead of cron, for journal integration and timezone-aware scheduling
- Idempotent operations at every layer
Three more problems worth naming
Getting Postiz into production
Nine interconnected containers with specific dependencies produced a cascade of environment-specific issues, about four hours across day two and early day three. The container and my own workstation had been handed the same address by DHCP, so the container was unreachable on the WAN. The router blocked WAN egress for certain container IPs, fixed with an iptables NAT MASQUERADE rule on the Proxmox host. pm2 zombie processes threw EADDRINUSE on backend restart, needing full container restarts. The login form silently failed to persist auth cookies because the Secure flag was set on a plain-HTTP deployment, fixed with NOT_SECURED=true. Instagram OAuth redirect URIs needed exact alignment with the Meta App’s whitelist. Each was diagnosed by reading logs.
Cross-platform command quoting
Administration ran from Windows PowerShell over SSH to the Proxmox host, then pct exec into a container, then sometimes docker exec into a container inside that. That four-layer chain strips quotes aggressively and inconsistently. SQL with embedded quotes, JSON payloads and sed expressions needed specific patterns to survive: single-quoted outer strings, stdin-piped payloads, PowerShell here-strings for multi-line bash, all documented for reuse.
The empty boxes
The first fully autonomous post rendered with all three checkmark boxes blank. The post-mortem was short: the OpenAI JSON schema had no bullets field. The prompt asked only for title, subtitle, caption, theme and hashtags, so the generator received nothing for those boxes and drew nothing. I added a required bullets array of exactly three items, 4 to 38 characters each, and updated the system prompt to explain their visual purpose: scannable, no full sentences, no trailing periods. The generator now accepts them, and the SharePoint override path got a fallback for rows with no bullets column. The next fire rendered correctly, verified live on Instagram.
Stack
| Layer | Technology |
|---|---|
| Backend / API | Python 3, Flask, systemd, nginx, gunicorn |
| AI / Content | OpenAI GPT-4o-mini (JSON structured output), OpenAI Image API |
| Databases | PostgreSQL 17, Redis 7.2, SQLite |
| Workflow engine | Temporal 1.28 (via Postiz), systemd timers |
| Scheduling | Postiz (self-hosted, open-source, AGPL) |
| Virtualization | Proxmox VE, LXC containers, Docker Compose |
| Networking | Cloudflare Tunnels, nginx reverse proxy, iptables NAT |
| External APIs | Instagram Graph API (Business Login), Microsoft Graph API (SharePoint), Meta OAuth2 |
| OS / Shell | Ubuntu 24, bash, PowerShell |
| Deployment | systemd services and timers, Docker Compose, logrotate |
Adding the next brand
Adding a second brand with its own Instagram account, topic list and brand voice takes about two hours: connect the channel to Postiz, copy the orchestrator to the target container, add a second systemd timer. It replaces 30 to 60 minutes of operator time per brand, every day.
If you want to know whether one of your own repeated processes is worth building this way, get in touch.
Is there something you do by hand every day
If there is a task you repeat daily, or a subscription you pay for something you could own, tell me about it. I will tell you whether it is worth automating and roughly what that would take.
Message on WhatsApp Send an email
Or call (941) 424-8081.