·7 min read

I'm deploying OpenClaw to run socials while I'm off grid.

AIDockerAutomation

From Docker Compose to Autonomous Content Creation

Docker, OpenClaw, a Telegram bot, and a tarot app walk into a VPS.


I'm planning a multi-week hiking trip in April. I also have a tarot app that needs consistent TikTok content. Those two things are incompatible — unless I can get an AI agent to handle the posting while I'm on the trail with no laptop.

The goal: a fully autonomous content pipeline that generates, schedules, and posts TikTok slideshows from a Docker container on a cheap VPS, controlled entirely through Telegram on my phone.

Here's how I set it up, and everything that went wrong along the way.

The Stack

  • OpenClaw — an open-source AI agent platform that runs in Docker and connects to messaging channels
  • Genviral — an OpenClaw skill that wraps a partner API for multi-platform social media posting (TikTok, Instagram, YouTube, etc.)
  • Telegram — the control interface, so I can talk to the bot from my phone on the trail
  • Claude Haiku 4.5 — the model running the agent (cheapest option for token conservation on long-running automation)
  • Docker — because everything that runs unattended should be containerized

The recovery plan is dead simple: private GitHub repo with all config (secrets excluded), so if the VPS dies, it's git clone && docker compose up -d on a new one.

The Setup Wasn't Smooth

OpenClaw's Docker setup has an interactive onboarding wizard. Great for humans clicking through a terminal. Not great when you're trying to automate things or run it headless. The wizard got stuck when I tried to run it in the background, so I ended up doing the onboarding manually and then editing config files directly.

The config file is openclaw.json, and it controls everything — model selection, channel settings, tool permissions, skill discovery. I edited it probably fifteen times before things worked. Every change requires a full docker compose down && docker compose up -d cycle. Not restart — I learned that the hard way.

The Telegram Rabbit Hole

Getting the Telegram bot to reliably receive and respond to messages was the most time-consuming part. Three separate issues stacked on top of each other:

The stale socket. Running Docker on Windows means WSL2 networking, which means the Telegram long-polling connection drops silently. The bot connects, handles one message, and then the polling loop dies without any error. The health check still passes because it only checks the HTTP endpoint, not the Telegram connection. From the outside, everything looks fine — the bot just stops responding.

The fix was changing the Telegram streaming mode from "partial" to "off" in the channel config. Partial streaming tries to send incremental message updates over the connection, and on WSL2 that connection is unreliable. With streaming off, the bot waits for the complete response and sends it as one message. Less fancy, but it actually works.

The ghost container. During initial setup, docker compose run creates one-off containers for the onboarding wizard. These containers don't get cleaned up by docker compose down — only containers defined in the compose file do. The orphan container was still running and polling the Telegram API with the same bot token, causing 409 conflicts. The gateway's polling loop would back off, retry, and eventually give up. Every restart just recreated the race condition.

Finding this required checking docker ps -a (not just docker ps) and noticing a container that had been running for 24 hours.

The self-inflicted wound. While debugging the polling issue, I was manually calling the Telegram getUpdates API from inside the container to check if it was working. Each manual call stole the polling session from the gateway, triggering more 409 conflicts. I was creating the exact problem I was trying to diagnose.

Skill Detection Is Picky

Genviral ships with a sub-skill called meta-ads in a nested directory. OpenClaw's skill scanner found the nested meta-ads/SKILL.md but skipped the parent genviral/SKILL.md. The bot could manage Meta ad campaigns but couldn't post to TikTok — the main thing I needed.

Two fixes were required: move meta-ads/ out to be a sibling directory instead of nested inside genviral/, and simplify the SKILL.md frontmatter. The original had extra fields like homepage and metadata.openclaw.requires that the scanner didn't handle. Stripping it down to just name and description made it load.

Even after fixing the files, the bot still didn't see the skill. OpenClaw caches the skills snapshot in sessions.json, so I had to delete that file too. And then delete it again after realizing the stale snapshot was separate from the session data files.

The Tool Profile Gotcha

The bot could see the genviral skill but couldn't use it. When it tried to read the SKILL.md file, it responded: "I don't have a direct file read tool available." The onboarding wizard had set the tool profile to "messaging", which only gives the agent chat-related tools — no file system access, no bash execution.

Changing tools.profile from "messaging" to "full" in openclaw.json gave the agent everything it needed: bash for running API commands, file read/write for loading skills and saving state, and all the session management tools.

The Payoff

After all that, asking the bot to list connected accounts produced a clean response in Telegram within seconds. The genviral script ran, the API returned results, and the message delivered. The whole pipeline — Telegram message in, skill loaded, bash command executed, API called, response formatted, message sent back — worked end to end.

The final setup is surprisingly minimal:

  • One docker-compose.yml with a single service
  • One Dockerfile that adds jq to the base image (the genviral scripts need it and it's not included)
  • One openclaw.json with all the config
  • Two skill directories
  • A .env file with secrets

Recovery is git clone, fill in secrets, docker compose up -d. The whole thing fits on a five-dollar Hetzner VPS.

What I'd Do Differently

Start with streaming: "off" on Telegram. The partial streaming mode is the default from onboarding and it silently breaks on WSL2. This cost me hours of debugging what looked like a Telegram connection issue.

Use tools.profile: "full" from the start. If your skills use bash commands (most do), the "messaging" profile will block them with no useful error message.

Keep skills flat. Don't nest skill directories. OpenClaw's scanner handles one level of depth cleanly; nesting causes unpredictable behavior.

Never touch the Telegram API manually while the gateway is running. Even read-only calls like getUpdates interfere with the polling loop.

Build a custom Docker image for any dependencies your skills need. Relying on post-start apt-get install means your bot is broken for the first 30 seconds after every restart, and fully broken after any container recreation.


The bot is currently in warmup mode — new TikTok accounts need 1-2 weeks of normal human usage before the algorithm will distribute their content properly. Once that's done, I'll set up the cron jobs for daily content generation, deploy to the VPS, and head for the trail.

The real test comes in April when I'm three days into a backcountry hike and the bot is still posting on schedule. I'll write about how that goes — assuming I have cell service to check.