A self-hosted Freeciv 3.2.3 multiplayer server designed for longturn games (23-hour turns), running on Fly.io with email notifications, a live status page, and an AI-generated newspaper.
An active 16-player game is running on this codebase right now. Check out the status page to see live rankings, turn countdowns, history charts, diplomacy tracking, and the AI-generated wartime newspaper.
Longturn is a style of Freeciv multiplayer where each turn lasts ~23 hours instead of minutes. Players log in once a day, make their moves, click "Turn Done", and go about their lives. When all players have ended their turn (or the timer runs out), the next turn begins.
┌─────────────────────────────────────────────────┐
│ Fly.io Container │
│ │
│ entrypoint.sh │
│ ├── busybox crond (status page refresh) │
│ └── start.sh │
│ ├── freeciv-server (port 5556) │
│ ├── busybox httpd (port 8080 → 80/443) │
│ ├── FIFO command writer │
│ ├── Turn change watcher │
│ ├── Auto-saver (every 5 min) │
│ └── Turn reminder checker │
│ │
│ /data/saves (persistent volume) │
│ ├── lt-game-*.sav.gz (save files) │
│ ├── freeciv.sqlite (player auth DB) │
│ ├── status.json (live game state) │
│ ├── history.json (per-turn stats) │
│ ├── attendance.json (missed turns) │
│ ├── diplomacy.json (relationships) │
│ └── gazette.json (AI newspaper) │
└─────────────────────────────────────────────────┘
The server communicates via a FIFO pipe (/tmp/server-input) — scripts send commands to the running Freeciv server by writing to this pipe.
| Script | Purpose |
|---|---|
entrypoint.sh |
Container entrypoint. Starts crond, then drops privileges and runs start.sh. |
start.sh |
Main orchestrator. Starts the Freeciv server, FIFO pipe, auto-save, turn watcher, reminder loop, HTTP server, and handles resume logic (preserving turn timer across restarts). |
longturn.serv |
Game settings: 23-hour turns, 10-hour unitwaittime, allied victory only, player list. |
| Script | Purpose |
|---|---|
generate_status_json.sh |
Extracts game state from save files into JSON. Runs every 5 minutes via cron and on each turn change. Produces status.json, history.json, attendance.json, and diplomacy.json. |
www/index.html |
Client-side status page. Fetches JSON and renders rankings, charts (Chart.js), diplomacy, countdown timer, and gazette articles. |
www/cgi-bin/health |
Healthcheck endpoint. Returns 503 if status.json is stale (>7 min), used by uptime monitors. |
| Script | Purpose |
|---|---|
turn_notify.sh |
Sends HTML email to all players when a new turn starts. Includes rankings table, gazette, and deadline. |
turn_reminder.sh |
Runs every 60 seconds. If within 2 hours of the deadline, sends a nudge email to players who haven't clicked "Turn Done". |
turn_notify.lua |
Freeciv signal handler that triggers turn_notify.sh on turn change. |
| Script | Purpose |
|---|---|
manage_players.sh |
Create player accounts in the SQLite auth DB, send welcome emails, list players. |
fcdb.conf / database.lua |
SQLite auth database configuration and initialization. |
| Script | Purpose |
|---|---|
fix_turn_timer.sh |
Override the turn deadline to a specific clock time (e.g., ./fix_turn_timer.sh 4 for 4 AM). Restores normal 23hr timeout on the next turn. |
change_gold.sh |
Adjust a player's gold via Lua command (e.g., ./change_gold.sh andrew 50). |
generate_gazette.sh |
Calls OpenAI to generate "The Civ Chronicle" — an era-appropriate, unreliable wartime newspaper article for each turn. |
generate_nations.sh |
Generates a static HTML page listing all available nations. |
local_preview.sh |
Preview the status page locally using save file data. |
| File | Purpose |
|---|---|
email_enabled.settings |
Set to true or false to toggle all email notifications. |
crontab |
Cron schedule — runs generate_status_json.sh every 5 minutes. |
fly.toml |
Fly.io deployment config (region, VM size, ports, volume). |
Dockerfile |
Multi-stage build: compiles Freeciv 3.2.3 from source, then creates a lean runtime image. |
- Fly.io CLI (
flyctl) - Docker (for local builds/testing)
- An AWS account with SES configured (for email notifications)
- An OpenAI API key (optional, for the AI gazette feature)
git clone <repo-url> cd freeciv-server cp .env.sample .env
Edit .env with your credentials:
SES_SMTP_USER=your-ses-smtp-username
SES_SMTP_PASS=your-ses-smtp-password
SES_SMTP_HOST=email-smtp.us-east-1.amazonaws.com
OPENAI_API_KEY=your-openai-key # optional, for gazette
Edit longturn.serv to configure your game:
timeout 82800— Turn length in seconds (82800 = 23 hours)unitwaittime 36000— Prevents double-moves (36000 = 10 hours)victories ALLIED— Victory conditions- Player list (
createcommands at the bottom)
Update email settings in the notification scripts:
FROM_EMAIL— The sender address (must be verified in SES)SERVER_HOST— Your server's hostnameCC_EMAIL— Optional CC address for all emails
Copy the sample players file and add your players:
cp players.conf.sample players.conf
Edit players.conf with one line per player:
PLAYERS=( "player1:pass123:player1@example.com:Australian" "player2:pass456:player2@example.com:Canadian" # ... add one line per player )
Format: "username:password:email:nation". This file is gitignored — credentials stay local.
You'll also need to add matching create commands in longturn.serv and aitoggle entries in start.sh for each player. See HOWTO-PROVISION-PLAYERS.md for the full walkthrough.
# Create the app fly launch --name your-app-name # Create a persistent volume for saves fly volumes create freeciv_saves --size 1 --region your-region # Set secrets (instead of hardcoding in scripts) fly secrets set \ SES_SMTP_USER=your-ses-smtp-username \ SES_SMTP_PASS=your-ses-smtp-password \ OPENAI_API_KEY=your-openai-key # Deploy fly deploy
Once deployed, provision all player accounts from your players.conf:
# Create all accounts and send welcome emails ./manage_players.sh create-all # Or add a single player ./manage_players.sh create username password email@example.com
This creates entries in the SQLite auth database and sends each player a welcome email with connection instructions.
6. Share Connection Details
Players connect using the Freeciv 3.2.3 client:
- Host: your-app-name.fly.dev
- Port: 5556
- Username/Password: as created above
The status page is available at https://your-app-name.fly.dev.
# Deploy changes fly deploy # SSH into the container fly ssh console --app your-app-name # Force a save fly ssh console --app your-app-name -C "sh -c 'echo save > /tmp/server-input'" # Regenerate the status page fly ssh console --app your-app-name -C "/opt/freeciv/generate_status_json.sh" # Check server logs fly ssh console --app your-app-name -C "tail -50 /data/saves/server.log" # Override turn deadline to 4 AM ./fix_turn_timer.sh 4 # Change a player's gold ./change_gold.sh playername 100 # Toggle emails off # Edit email_enabled.settings to "false" and redeploy # Restart the server (preserves turn timer) fly apps restart your-app-name
The most reliable way to change game state mid-game is editing the save file directly. FIFO commands get garbled beyond ~200 characters, and many server commands are blocked mid-game.
# 1. Force a save fly ssh console --app your-app-name -C "sh -c 'echo save > /tmp/server-input; sleep 3'" # 2. Download it fly ssh console --app your-app-name -C "cat /data/saves/save-latest.sav.gz" > /tmp/save.sav.gz gzip -dc /tmp/save.sav.gz > /tmp/save.txt # 3. Edit /tmp/save.txt (it's plaintext INI-style) # 4. Upload and restart gzip -c /tmp/save.txt > /tmp/save-edited.sav.gz cat /tmp/save-edited.sav.gz | base64 | fly ssh console --app your-app-name \ -C "sh -c 'base64 -d > /data/saves/save-latest.sav.gz'" fly apps restart your-app-name
The server preserves the turn timer across restarts and redeploys. On resume, start.sh:
- Reads
phase_seconds(time elapsed in the current turn) from the save file - Calculates remaining time:
timeout - phase_seconds - Restores the correct deadline so players don't lose time
| Variable | Required | Default | Description |
|---|---|---|---|
SES_SMTP_USER |
For emails | — | AWS SES SMTP username |
SES_SMTP_PASS |
For emails | — | AWS SES SMTP password |
SES_SMTP_HOST |
No | email-smtp.us-east-1.amazonaws.com |
SES SMTP endpoint |
OPENAI_API_KEY |
For gazette | — | OpenAI API key for AI newspaper |
SERVER_HOST |
No | freeciv.andrewmcgrath.info |
Server hostname (for emails/status page) |
FROM_EMAIL |
No | freeciv@andrewmcgrath.info |
Sender email address |
Set these as Fly.io secrets for production:
fly secrets set SES_SMTP_USER=... SES_SMTP_PASS=... OPENAI_API_KEY=...├── Dockerfile # Multi-stage build (compile Freeciv + runtime)
├── fly.toml # Fly.io config
├── entrypoint.sh # Container entrypoint
├── start.sh # Server startup orchestrator
├── longturn.serv # Game settings
├── fcdb.conf # Auth DB config
├── database.lua # DB initialization
├── crontab # Scheduled tasks
├── email_enabled.settings # Email toggle
├── turn_notify.lua # Turn change signal handler
├── generate_status_json.sh # Status page data pipeline
├── generate_gazette.sh # AI newspaper generator
├── generate_nations.sh # Nations list page
├── turn_notify.sh # Turn email notifications
├── turn_reminder.sh # Deadline reminder emails
├── manage_players.sh # Player account management
├── fix_turn_timer.sh # Manual deadline override
├── change_gold.sh # Gold adjustment utility
├── local_preview.sh # Local testing helper
├── .env.sample # Environment variables template
├── www/
│ ├── index.html # Status page (JS-rendered)
│ ├── changelog.html # Game changelog
│ └── cgi-bin/
│ └── health # Healthcheck endpoint
└── CLAUDE.md # Operations reference