Here's what it took to make OpenClaw bulletproof
Making OpenClaw Bulletproof
Standing up an autonomous TikTok bot in Docker, then hardening it to survive a month on a DigitalOcean droplet with no one watching.
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, everything that went wrong along the way, and what it took to make it bulletproof enough to survive a month unattended.
Part 1 — From Docker Compose to Autonomous Content Creation
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. 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 local setup was surprisingly minimal:
- One
docker-compose.ymlwith a single service - One
Dockerfilethat addsjqto the base image (the genviral scripts need it and it's not included) - One
openclaw.jsonwith all the config - Two skill directories
- A
.envfile with secrets
Recovery is git clone, fill in secrets, docker compose up -d.
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.
Part 2 — From Local Docker Container to DigitalOcean VPS
In part 1, I got the bot working locally — OpenClaw running in Docker on Windows, talking to Telegram, posting TikTok content through the Genviral skill. It worked. Until it didn't.
The problem with "works on my machine" is that my machine runs WSL2, and WSL2's networking has opinions about long-lived connections. The Telegram polling loop would die every few hours — silently, with no errors, while the health check kept reporting everything was fine. I'd notice hours later when I checked my phone and saw the bot had gone quiet.
That's not going to cut it for a month on a trail with no laptop.
Leaving Windows Behind
The fix was obvious: get off WSL2 entirely. A proper Linux VPS with real networking, real Docker, and no virtualization layer randomly killing TCP connections.
I went with DigitalOcean — a 2 vCPU, 2GB RAM droplet running Ubuntu 24.04. Twelve dollars a month. The entire provisioning took one doctl command:
doctl compute droplet create mystic-bot \
--image ubuntu-24-04-x64 \
--size s-2vcpu-2gb \
--region nyc1 \
--ssh-keys <fingerprint>
Forty seconds later I had a server. SSH in, install Docker, clone the repo, docker compose up -d. The gateway came up healthy on the first try. Telegram connected immediately and stayed connected. No stale sockets, no silent drops, no hourly restarts.
The WSL2 networking issue that consumed most of my debugging time in part one just... wasn't a thing anymore. Turns out the fix for unreliable virtualized networking is to not use virtualized networking.
Building the Safety Net
A working bot is step one. A bot that stays working for a month without human intervention is a different problem. I needed to think about every way it could fail and build recovery for each one.
Docker handles process crashes. The compose file already had restart: unless-stopped, so if the gateway process dies, Docker restarts it automatically. I tested this by killing PID 1 inside the container — it was back up and healthy in under twenty seconds.
Systemd handles server reboots. systemctl enable docker means Docker starts on boot, and Docker starts the container because of the restart policy. I rebooted the droplet to test — the bot was responding on Telegram within forty seconds of the server coming back up.
Unattended upgrades handle OS patches. Ubuntu's unattended-upgrades package applies security updates automatically. I disabled automatic reboots — a kernel update shouldn't take the bot offline. If a reboot is genuinely needed, the restart chain handles it anyway.
A watchdog cron handles the sneaky failures. This is the one that matters most. Sometimes everything looks healthy but the Telegram connection has gone stale. The health check passes, Docker thinks the container is fine, but the bot isn't receiving messages.
The watchdog runs every five minutes. It checks three things: is the container running, is it healthy, and has there been any Telegram activity in the logs recently. If the container is down, it starts it. If it's unhealthy or Telegram has gone quiet, it does a full down/up cycle — the same fix that works manually, just automated.
UFW locks down the attack surface. The only inbound port is SSH. The gateway doesn't need to be publicly accessible because Telegram uses outbound polling — the bot calls Telegram, not the other way around.
The Panic Button
All that automation covers normal failures. But what about the scenario where I'm on a trail, I check Telegram, and the bot isn't responding? I can't SSH in from my iPhone without an unreasonable amount of setup.
So I built a webhook. A tiny Python HTTP server running on the droplet behind a secret URL. Open it in Safari, it restarts the bot. One tap.
The important part is making it safe to use carelessly. Tapping the link twice shouldn't cause two overlapping restarts. Tapping it while the watchdog is already restarting shouldn't create a conflict. So the webhook has a lockfile that prevents concurrent restarts and a five-minute cooldown after each restart. If you hit it during cooldown, it tells you when to try again instead of doing anything.
The URL is a 40-character random hex string — essentially a password in the path. Wrong URL gets a 404 with no information. It runs as a systemd service so it survives reboots, same as everything else.
I bookmarked it on my phone. Hopefully I never use it.
Keeping It Clean
The last concern is disk space. The droplet has 50GB, and Docker logs plus OpenClaw session files could theoretically fill that over a month. Two mitigations:
Docker's log rotation caps each container's logs at 150MB (three 50MB files, auto-rotated). That's configured in /etc/docker/daemon.json and applies globally.
Session files — the conversation logs that give the bot context about previous chats — get pruned weekly, but only if they exceed 5GB total. Under that threshold, everything is preserved. I wanted the bot to keep as much context as possible in case it needs to reference something from a conversation two weeks ago, but not at the risk of filling the disk while I'm unreachable.
The Final Checklist
Before calling this done, I verified every recovery path:
- Kill the gateway process → Docker restarts it in 20 seconds
- Reboot the entire server → everything comes back in 40 seconds
- Hit the webhook → clean restart with cooldown protection
- Leave it running overnight → still responding in the morning
genviral accounts→ TikTok account active, API working
The bot is live on the VPS now. The local Docker setup on my Windows machine is shut down. Everything runs from a server in a New York data center that doesn't care about WSL2 networking quirks.
What's Left
The TikTok account is still in its warmup period — new accounts need organic-looking activity before the algorithm will distribute their content properly. Once that's done, I need to set up the actual content scheduling. OpenClaw has a built-in cron system, and Genviral has its own calendar layer. I haven't wired those up yet.
But the core infrastructure is done. The bot is running, the recovery paths are tested, and the server is ready for the real work.