I recorded a run-through of setting up the ICS → Discourse sync on a fresh DigitalOcean droplet.
This reply shares a timeline of what I did, with clickable YouTube timestamps so you can jump straight to the relevant parts of the video:
Timestamp | Description/Command Issued |
---|---|
0:12 | DO Droplet image selected as Ubuntu 24.04 (LTS), not 25.04 ![]() |
1:33 | ufw enabled with my usual configuration |
2:47 | Discourse main repository with standalone.yml cloned to DO droplet |
3:01 | Created an A record from namecheap to ipv4 of DO droplet |
3:35 | A record didn’t propagate in time, so ./discourse-setup connection to the allowed https port fails |
4:03 | Manually change DISCOURSE_HOSTNAME in app.yml , forgot to uncomment 2 Let’s Encrypt lines |
4:28 | Changed SMTP setting for dummy, might not be necessary? |
4:55 | ./launcher rebuild app starts |
9:46 | ./launcher rebuild app finishes |
10:18 | ran rake admin:create because didn’t bother setting up SMTP |
12:57 | arrived in discourse but without https |
13:32 | calendar_enabled admin site setting |
13:39 | added general as a calendar category |
14:03 | Discourse post event enabled. Next time, change max tag length from 20 to 30 aswell |
14:15 | was made aware that general has category id 4 |
15:33 | apt install -y python3 python3-venv python3-pip curl nano ca-certificates |
16:46 | mkdir -p /opt/ics_sync |
16:58 | chown $USER:$USER /opt/ics_sync |
17:08 | cd /opt/ics_sync |
17:19 | python3 -m venv venv |
17:35 | source venv/bin/activate |
17:47 | pip install --upgrade pip |
17:59 | pip install requests python-dateutil icalendar |
18:33 | cat > /opt/ics_sync/.env <<'EOF' |
18:51 | export DISCOURSE_BASE_URL="https://your.forum.url" |
19:18 | export DISCOURSE_API_KEY="YOUR_DISCOURSE_API_KEY" |
19:27 | setup API key for system user |
19:34 | make it granular, should have also ticked “tags → list” |
20:38 | ran command with relevant API key |
20:43 | export DISCOURSE_API_USERNAME="system" |
20:51 | export ICS_SOURCE="https://example.com/feed.ics" |
21:21 | export DISCOURSE_CATEGORY_ID=4 |
21:23 | export SITE_TZ="Europe/London" |
21:27 | export DEFAULT_TAGS="events,ics" |
(missing) | export ICS_NAMESPACE="uon-mycal" however its import is missing on the script |
21:38 | EOF |
22:00 | cd /opt/ics_sync |
22:07 | set -a |
22:12 | source .env |
22:17 | set +a |
22:25 | nano /opt/ics_sync/ics_to_discourse.py |
22:47 | chmod +x /opt/ics_sync/ics_to_discourse.py |
curl -sS -X POST "$DISCOURSE_BASE_URL/posts.json" \
-H "Api-Key: $DISCOURSE_API_KEY" \
-H "Api-Username: $DISCOURSE_API_USERNAME" \
-F "title=API test $(date +%s)" \
-F 'raw=[event start="2025-10-10T10:00:00Z" end="2025-10-10T11:00:00Z" timezone="Europe/London"]Test[/event]' \
-F "category=$DISCOURSE_CATEGORY_ID"
If https is working on your Discourse, the above API test works fine.
Timestamp | Description/Command Issued |
---|---|
24:19 | crontab -e |
25:00 | 1 (cron job added) |
26:35 | check the status of cron in systemctl |
27:18 | Destroy DO Droplet |
Conclusion:
This exercise shows that the script works end-to-end with a clean Ubuntu 24.04 droplet.
I still need to add support for ICS_NAMESPACE
in the script (to avoid tag collisions across feeds), but otherwise the setup went smoothly.
Big thanks to everyone who contributed improvements on this thread — hopefully the timestamps + video help others get it running more quickly.