Systemd vs PM2, webhook timeouts, SSL setup, and all the other small things that cause 3am wake-up calls if you get them wrong.
When you first deploy a LINE bot backend, the happy path is easy. Flask app running in a screen session, ngrok for HTTPS, it all works fine in development. Production is a different story. Servers restart, processes crash, webhook timeouts trip you up, SSL certs expire. Getting a LINE bot to run reliably 24/7 requires a bit more structure.
Here is the stack we settled on after a few painful iterations.
Process management with PM2
PM2 is a Node-based process manager that works perfectly well for Python processes too. The main advantage over systemd for our use case is the easy log management, automatic restarts, and the ability to manage multiple services from one place.
npm install -g pm2
# Start Flask with PM2
pm2 start "gunicorn -w 2 -b 127.0.0.1:5000 app:app" --name line-bot
# Auto-start on reboot
pm2 startup
pm2 saveWe use Gunicorn instead of Flask's dev server because it handles concurrent webhook requests properly. A single-worker Flask dev server will queue up incoming webhooks, and if your handler is slow, LINE will start timing out and retry, which makes things worse.
Two workers is usually enough for a bot handling a few hundred users. Scale up if you are handling thousands of messages per minute.
nginx as reverse proxy
LINE webhooks require HTTPS. You cannot use a bare HTTP endpoint. The easiest way to handle this on a VPS is nginx as a reverse proxy with Let's Encrypt for SSL.
server {
listen 443 ssl;
server_name your-bot-domain.com;
ssl_certificate /etc/letsencrypt/live/your-bot-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-bot-domain.com/privkey.pem;
location /webhook {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_read_timeout 30s;
proxy_connect_timeout 10s;
}
}The timeout settings matter. LINE expects your webhook endpoint to respond within 30 seconds. If it takes longer, LINE marks the delivery as failed and may retry. Set proxy_read_timeout slightly under LINE's timeout so your server has time to return a 200 before LINE gives up.
Webhook handling pattern
One of the most common mistakes is doing heavy work synchronously in the webhook handler. LINE sends a POST and waits. If you are doing database queries, external API calls, or anything that takes more than a second or two, you will hit timeout issues.
The right pattern is to acknowledge the webhook immediately and process the event in a background thread:
from flask import Flask, request
import threading
app = Flask(__name__)
def process_event(event_data):
# Your actual logic here
pass
@app.route("/webhook", methods=["POST"])
def webhook():
body = request.get_json()
# Spawn background thread and return immediately
thread = threading.Thread(target=process_event, args=(body,))
thread.daemon = True
thread.start()
return "OK", 200For anything more complex, use a proper task queue like Celery with Redis. But for most bot use cases, a background thread is fine.
Cert renewal
Use certbot with the Cloudflare DNS plugin if your domain is on Cloudflare. It handles wildcard certs and renews automatically without needing to expose port 80:
pip install certbot-dns-cloudflare
certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cloudflare.ini -d your-bot-domain.comSet up a cron job to renew monthly:
0 3 1 * * certbot renew --quiet && pm2 restart nginx-like-serviceWith this setup, bots stay running through reboots, handle concurrent requests properly, and never have an expired cert. Most of the 3am incidents we used to have were caused by one of these three things.