Following my inability to bring the new server online based on a copy of the whole filesystem (see 2026-02-26 Clone a virtual machine from inside it), I'm going to be bringing it online manually, domain name by domain name, service by service. And as in previous years, I'm going to try and document it here. For my future self. To channel my disappointment.
2026-02-26 Clone a virtual machine from inside it
Looking at my computer naming scheme, I picked a new name for the server: Paraelectrobombus.
my computer naming scheme
Locale
Add `de_CH.UTF-8` and `en_GB.UTF8`.
Time
Pick `Europe`/`Zurich`.
Software to install
apt install \
amfora apache2 bc colordiff ed emacs fail2ban fish \
gawk-doc gnutls-bin gnutls-doc htop info inn2 \
libarray-utils-perl libcommonmark-perl \
libdatetime-format-iso8601-perl libdatetime-format-mail-perl \
libfile-slurper-perl libgd-perl libi18n-acceptlanguage-perl libmodern-perl-perl \
libmastodon-client-perl libminion-perl libminion-backend-sqlite-perl \
libmojolicious-perl libmojolicious-plugin-authentication-perl libmojolicious-plugin-cgi-perl \
libnet-ip-perl libnet-whois-parser-perl \
libpoe-component-irc-perl libpoe-component-sslify-perl \
libsort-versions-perl \
libtext-autoformat-perl libtext-markdown-perl \
libwww-perl locate make makepasswd mc monit moreutils munin \
ncat nftables nodejs \
pipx python3-bs4 python3-pyasn python3-pygments python3-netaddr python3-mastodon python3-html2text \
python3-bcrypt python3-argon2 radicale rlwrap \
shellcheck sudo sqlite3 sqlite3-tools sqlite3-doc stunnel4 \
tin w3m
Add backports:
# cat /etc/apt/sources.list.d/debian-backports.sources
Types: deb deb-src
URIs: http://deb.debian.org/debian
Suites: trixie-backports
Components: main
Enabled: yes
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Install `asncounter`:
apt update
apt install asncounter/trixie-backports
Yay!
Environment
Switch to the `fish` shell:
chsh --shell /usr/bin/fish
Copy stuff for root:
rsync -ai --exclude=.ssh sibirocobombus:/root/ .
Limit the logs
# diff-backup /etc/systemd/journald.conf
27c27
< #SystemMaxUse=
---
> SystemMaxUse=200M
35c35
< #MaxRetentionSec=0
---
> MaxRetentionSec=7d
Hostname
Rename a computer on the Debian wiki.
Rename a computer
echo paraelectrobombus > /etc/hostname
sed -i~ 's/ $/ paraelectrobombus/' /etc/hosts
hostnamectl set-hostname paraelectrobombus
reboot
SSH
Facilitate the connection to the old server:
ssh-keygen
cat ~/.ssh/id_ed25519.pub
Copy and this line and append it to `~/.ssh/authorized_keys` on the old server.
On the new server:
echo -e "Host sibirocobombus\nHostName alexschroeder.ch\nPort 882" >> ~/.ssh/config
`sshd`
Prevent login using passwords. Make sure you have authorized the necessary keys! If you ever loose access to them, use the rescue system or a VNC connection to allow passwords again.
Also less printing when I log in. I do it a lot. And switch the ssh port so that the logs aren't too busy.
# cat /etc/ssh/sshd_config.d/paraelectrobombus.conf
# Obscure port to reduce log noise.
Port 882
# No passwords allowed
PasswordAuthentication no
GatewayPorts yes
# Don bother with the last login
PrintLastLog no
Just to be sure, allow the following users only:
# cat /etc/ssh/sshd_config.d/users.conf
# List of user names allowed to log in.
# We need git for git-via-SSH repos.
AllowUsers git root
Apache
Remove default site, remove CGI scripts, enable `mod_md`.
a2disconf charset localized-error-pages other-vhosts-access-log serve-cgi-bin
a2dissite 000-default
for f in conf-available conf-site hook.sh gate.pw md
rsync -ai sibirocobombus:/etc/apache2/$f /etc/apache2/
end
for f in fuck-ai fuck-google hardening letsencrypt log max-uri mdomain security
rsync -ai sibirocobombus:/etc/apache2/conf-available/$f.conf \
/etc/apache2/conf-available/
a2enconf $f
end
echo ServerName (hostname) > /etc/apache2/conf-available/servername.conf
a2enconf servername
a2enmod headers ssl rewrite proxy proxy_http cgid
apachectl configtest
There is no point in restarting the server just yet.
What's important is that I use `mod_md` to manage the certificates for my sites.
# cat /etc/apache2/conf-available/mdomain.conf
# Each site has its own MDomain option, but these options are for all of them
MDCertificateAgreement accepted
MDMessageCmd /etc/apache2/hook.sh
Thus, whenever Apache makes changes, I might have to update copies of the certificates elsewhere. This copy of `/etc/apache2/hook.sh` includes services that haven't migrated to the new server, yet.
#!/bin/bash
domain_dir=/etc/apache2/md/domains
if [ -z "$2" ]; then
echo Needs event and domain, e.g. hook.sh installed alexschroeder.ch
exit
fi
event="$1"
domain="$2"
if [ $event == "errored" ]; then
# Possibly Let's Encrypt bot is being banned
echo "Adding Let's Encrypt IP addresses to the allow-list"
/etc/butlerian-jihad/letsencrypt-prepare \
| tee /etc/butlerian-jihad/letsencrypt.nft \
| nft -f -
elif [ $event == "installed" ]; then
# Possibly reloading once for every domain in very short order? 🤔
service apache2 reload
if [ $domain == "korero.org" ]; then
echo "Regenerating monit's .pem file..."
cat $domain_dir/$domain/*.pem > /etc/ssl/localcerts/korero.org.all.pem
systemctl reload monit
elif [ $domain == "campaignwiki.org" ]; then
echo "Importing certs for prosody..."
cat $domain_dir/$domain/privkey.pem > /etc/prosody/certs/campaignwiki.org.privkey.pem
cat $domain_dir/$domain/pubcert.pem > /etc/prosody/certs/campaignwiki.org.fullchain.pem
chown prosody:prosody /etc/prosody/certs/*.pem
chmod go-rw /etc/prosody/certs/campaignwiki.org.privkey.pem
systemctl reload prosody
echo "Importing certs for ngircd..."
cat $domain_dir/$domain/privkey.pem > /etc/ngircd/key.pem
cat $domain_dir/$domain/pubcert.pem > /etc/ngircd/cert.pem
chown irc:irc /etc/ngircd/*.pem
chmod go-rw /etc/ngircd/key.pem
systemctl reload ngircd
echo "Importing certs for galene..."
cat $domain_dir/$domain/privkey.pem > /home/galene/data/key.pem
cat $domain_dir/$domain/pubcert.pem > /home/galene/data/cert.pem
chown galene:nogroup /home/galene/data/*.pem
chmod go-rw /home/galene/data/key.pem
systemctl restart galene
echo "Reloading stunnel..."
systemctl reload stunnel4
fi
echo "Granting permissions to the ssl-cert group..."
chmod g+r $domain_dir/$domain/*.pem
fi
The `conf-site` directory contains config files that need to be included for some locations.
This is a file that locks and unlocks sites. The default is for the site to be unlocked. A timer starts a service every few minutes that decides whether to switch it on or off.
# cat /etc/apache2/conf-site/gate.conf
Require all granted
The bot-check configuration is also documented on the Butlerian Jihad page.
Butlerian Jihad
# cat /etc/apache2/conf-site/botcheck.conf
# Handle non-JavaScript confirmation POST (preserve query string; host must match)
RewriteCond %{REQUEST_METHOD} POST
RewriteCond %{HTTP_REFERER} ^https?://([^/:]+)(?::[0-9]+)?(/[^?]*)(\?.*)?$ [NC]
RewriteCond %{HTTP_HOST} ^%1 [NC]
RewriteRule ^/botcheck-confirm$ %2%3 [L,R=303,NE,E=BOTCHECK_CONFIRM:1,UnsafeAllow3F]
# Fallback redirect when no Referer
RewriteCond %{REQUEST_METHOD} POST
RewriteRule ^/botcheck-confirm$ / [L,R=303,E=BOTCHECK_CONFIRM:1]
# Set cookie for confirmed users
Header always set Set-Cookie "botcheck=1; Max-Age=2592000; Path=/; SameSite=Lax" env=BOTCHECK_CONFIRM
# The botcheck page, served with 402 status code when direct access is denied
Alias /botcheck /var/www/html/botcheck.html
ErrorDocument 402 /botcheck
<Location /botcheck>
Require all granted
</Location>
RewriteCond %{REQUEST_URI} ^/botcheck$
RewriteRule .* - [L]
# Skip botcheck during error handling subrequests
RewriteCond %{ENV:REDIRECT_STATUS} !^$
RewriteRule .* - [E=BOTCHECK:OK]
# Allow non-GET methods without botcheck
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteCond %{REQUEST_METHOD} !GET
RewriteRule .* - [E=BOTCHECK:OK]
# Skip access control for the main Oddmuse feeds
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteCond %{QUERY_STRING} action=(rss|journal)
RewriteRule /(emacs|wiki)(/[^/]*)? - [E=BOTCHECK:OK]
# Skip access control for feeds incl. podcasts
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteCond %{REQUEST_URI} .*\.(rss|xml|opml|mp3)$
RewriteRule .* - [E=BOTCHECK:OK]
# Skip access control for everything in campaignwiki.org/files for feeds, podcasts and calendars
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteCond %{HTTP_HOST} =campaignwiki.org
RewriteCond %{REQUEST_URI} /files(/.*)?$
RewriteRule .* - [E=BOTCHECK:OK]
# Set environment variable for allow-listed IPs
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteCond %{REMOTE_ADDR} 213.160.77.191 [ornext]
RewriteCond %{REMOTE_ADDR} 2a00:17d8:100::4ab1
RewriteRule .* - [E=BOTCHECK:OK]
# Set environment variable for allow-listed User-Agents
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteCond %{HTTP_USER_AGENT} ^(Monit|Dillo)
RewriteRule .* - [E=BOTCHECK:OK]
# Set environment variable for valid cookie
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteCond %{HTTP_COOKIE} botcheck
RewriteRule .* - [E=BOTCHECK:OK]
# Return 402 status and the botcheck page if none of the conditions are met
RewriteCond %{ENV:BOTCHECK} !^OK$
RewriteRule .* - [L,R=402]
This requires a form:
# cat /var/www/html/botcheck.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Bot Check</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.consent-box {
border: 1px solid #ddd;
padding: 30px;
border-radius: 8px;
background: #f9f9f9;
}
button {
background: #007cba;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #005a87;
}
form {
display: flex;
flex-direction: column;
gap: 12px;
}
h1 {
margin-top: 0;
}
small {
text-align: right;
display: block;
margin-top: 40px;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="consent-box">
<h1>Are you Human?</h1>
<p>
Silly question, but are you actually a human?
I'm still having troubles with the mindless bots scraping the web in the service of AI companies,
so be sure to press the right button.
</p>
<form action="/botcheck-confirm" method="post">
<input type="hidden"/>
<button type="submit">Yes, I'm a real person.</button>
</form>
<p>
If you're using <tt>links2</tt> and you've clicked
the button, reload using <tt>Ctrl+R</tt> and you
should get redirected.
</p>
<noscript>
<p>No JavaScript, relying on referrer header.</p>
</noscript>
<small>Powered by splitbrain's <a href="https://github.com/splitbrain/botcheck">botcheck</a>.</small>
</div>
<script>
document.getElementsByTagName('form')[0].addEventListener('submit', (ev) => {
ev.preventDefault();
// Set cookie for 30 days
const expires = new Date();
expires.setTime(expires.getTime() + (30 * 24 * 60 * 60 * 1000));
document.cookie = "botcheck=1; expires=" + expires.toUTCString() + "; path=/; SameSite=Lax";
// Reload the page to continue with original request
window.location.reload();
});
</script>
</body>
</html>
This is a file to block a lot of bots by user agent:
# cat /etc/apache2/conf-site/blocklist.conf
RewriteEngine on
# Block fediverse instances asking for previews
RewriteCond "%{HTTP_HOST}" "!^social"
RewriteCond "%{HTTP_USER_AGENT}" "Mastodon|Friendica|Pleroma|Akkoma|Misskey" [nocase]
RewriteRule "^" - [forbidden,last]
# The 410 GONE response also contains garbage
ErrorDocument 410 /nobot/
RewriteRule "^/nobot/.*" "unix:/run/garbage.sock|http://localhost/" [proxy,last]
# Block all bots and crawlers, except the Let's Encrypt bot
RewriteCond "%{HTTP_USER_AGENT}" "!archive\.org_bot|^gwene|wibybot|ecosia|qwantbot|check_http" [nocase]
RewriteCond "%{HTTP_USER_AGENT}" "bot|crawler|spider|ggpht|gpt" [nocase]
# (redirect to /nobots means fail2ban is watching)
RewriteRule "^" - [redirect=410,last]
# SEO bots, borked feed services and other shit
RewriteCond "%{HTTP_USER_AGENT}" "eyemonit|linkfluence|megaindex|pcore|synapse|wiederfrei|\bads|feedburner|brandwatch|openai|facebookexternalhit|yisou|googleother|meta-externalagent|fuzz faster|uptime-kuma|meta-externalagent|wellknownbot|7siters" [nocase]
# (redirect to /nobots means fail2ban is watching)
RewriteRule "^" - [redirect=410,last]
# Block for GET requests for search, recent changes and filtered feeds
RewriteCond "%{QUERY_STRING}" "search=" [or]
RewriteCond "%{QUERY_STRING}" "action=rc" [or]
RewriteCond "%{QUERY_STRING}" "action=rss[&;]"
RewriteRule "^" - [redirect=410,last]
# Block the wordpress bots.
RewriteRule "/wlwmanifest\.xml$" - [redirect=410,last]
# Block all idiots that are looking for borked PHP applications
# (redirect to /nobots means fail2ban is watching)
RewriteRule "\.php$" - [redirect=410,last]
# Deny the weird image scraper
# https://imho.alex-kunz.com/2024/02/25/block-this-shit/
RewriteCond "%{HTTP_USER_AGENT}" "Firefox/72.0" [nocase]
# (redirect to /nobots means fail2ban is watching)
RewriteRule ^ - [redirect=410,last]
# Any locations starting with /nobot/
RewriteRule "^/nobot/.+" - [redirect=410,last]
Garbage
The previous section uses `/run/garbage` for the bots. Here's where that comes from.
Build garbage and install it as `/usr/local/bin/garbage`.
garbage
Use a system user.
Socket:
# cat /srv/garbage/garbage.socket
[Unit]
Description=Garbage server socket
[Socket]
ListenStream=/run/garbage.sock
Accept=no
[Install]
WantedBy=sockets.target
Service:
# cat /srv/garbage/garbage.service
[Unit]
Description=Garbage
After=network.target
Requires=garbage.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
DynamicUser=true
ExecStart=/usr/local/bin/garbage --file /srv/garbage/moby-dick.txt
You can use any text file you want for the garbage generation. I downloaded a copy of Moby Dick. Sorry!
Test the socket:
echo -e "GET /search HTTP/1.1\r\nHost: localhost\r\n\r\n" \
| ncat --unixsock /run/garbage.sock
Monit
This uses the korero.org certificates, as shown by the `hook.sh` file above.
cd /etc/monit/conf-available
for f in apache2 cron openssh-server
sed -i~ 's/service \([^ ]*\) \(start\|stop\)/\/usr\/bin\/systemctl \2 \1/' $f
end
cd /etc/monit/conf-enabled
for f in apache2 cron openssh-server
ln -s ../conf-available/$f .
end
mkdir -p /etc/ssl/localcerts
cat /etc/apache2/md/domains/korero.org/*.pem > /etc/ssl/localcerts/korero.org.all.pem
chmod 600 /etc/ssl/localcerts/korero.org.all.pem
The config file, with a different password, of course.
# cat /etc/monit/conf.d/monit.conf
set httpd port 2812 and
with ssl {
pemfile: /etc/ssl/localcerts/korero.org.all.pem
}
allow admin:*secret*
Restart and check the log:
systemctl restart monit
tail /var/log/monit.log
And now `https://korero.org:2812/` should work.
Munin
Setup an Apache config file:
# cat /etc/apache2/conf-available/munin.conf
Alias /munin /var/cache/munin/www
# Static files are free for all to see
<Directory /var/cache/munin/www>
Require all granted
</Directory>
ScriptAlias /munin-cgi/munin-cgi-graph /usr/lib/munin/cgi/munin-cgi-graph
# CGI script requires login
<Directory /usr/lib/munin/cgi>
Order allow,deny
Allow from all
Options None
AuthUserFile /etc/munin/munin-htpasswd
AuthName "Munin"
AuthType Basic
require valid-user
<IfModule mod_fcgid.c>
SetHandler fcgid-script
</IfModule>
<IfModule !mod_fcgid.c>
SetHandler cgi-script
</IfModule>
</Directory>
Notice how the above relies on a CGI module.
Get the password file:
rsync -ai sibirocobombus:/etc/munin/munin-htpasswd /etc/munin/
Enable it:
a2enconf munin
systemctl reload apache2
Fix the hostname, add my own modules and their config:
my own modules
sed -i~ 's/localhost.localdomain/paraelectrobombus/' /etc/munin/munin.conf
rsync -ai sibirocobombus:/usr/local/share/munin /usr/local/share/
find /usr/local/share/munin/plugins/ -maxdepth 1 -type f ! -name '*~' \
-exec ln -sf {} /etc/munin/plugins/ ';'
rsync -ai sibirocobombus:/etc/munin/plugin-conf.d/ /etc/munin/plugin-conf.d
systemctl restart munin-node
Using the korero.org domain that's set up below: Munin.
Munin
Butlerian Jihad including fail2ban
Note that GoToSocial hasn't migrated, yet. That's why the allow-list can be empty.
rsync -ai sibirocobombus:/etc/fail2ban /etc/
rsync -ai sibirocobombus:/var/lib/fail2ban /var/lib/
rsync -ai sibirocobombus:/etc/butlerian-jihad /etc/
systemctl restart fail2ban
echo > /etc/butlerian-jihad/gotosocial.nft
Double-check:
Install the watchers and their timers but not the services to publish the `butlerian-jihad-week` fail2ban jail and not the `gotosocial-prepare` service.
systemctl enable --now ./gate.service ./gate.timer ./watch*.service ./watch*timer
There is a system user of the same name that owns the directory.
adduser gomphotherium --system
mkdir /srv/gomphotherium
Service:
# cat /srv/gomphotherium/gomphotherium.service
[Unit]
Description=Gomphotherium
[Service]
Type=oneshot
# The credentials are in $HOME/.config/toot/config.json
Environment=HOME=/srv/gomphotherium
ExecStart=@/srv/gomphotherium/gomphotherium gomphotherium --verbose alex@social.alexschroeder.ch blog@social.alexschroeder.ch osr@social.alexschroeder.ch alex@rollenspiel.social kensanata@tabletop.social
Timer:
# cat /srv/gomphotherium/gomphotherium.timer
[Unit]
Description=Gomphotherium
[Timer]
OnCalendar=*-*-* 03:10:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
The script itself is from the Gomphotherium repository.
Gomphotherium repository
Since `rss-bot` needs the same credentials, I'm going to put the service here, too. Install it from the repo into `/usr/local/bin`.
the repo
The service needs to call all commands and return an error if any one of them failed. Since there is no rush, run them one after another using `&&` in a shell:
# cat /srv/gomphotherium/rss-bot.service
[Unit]
Description=RSS Bot
[Service]
Type=oneshot
User=gomphotherium
WorkingDirectory=/srv/gomphotherium
Environment=HOME=/srv/gomphotherium
ExecStart=sh -c "rss-bot planet@social.alexschroeder.ch https://campaignwiki.org/rpg/feed.xml && \
rss-bot osr@social.alexschroeder.ch https://campaignwiki.org/files/osr-discord.xml && \
rss-bot bookmarks@social.alexschroeder.ch https://alexschroeder.ch/wiki/bookmarks.rss && \
rss-bot blog@social.alexschroeder.ch https://alexschroeder.ch/view/index.rss"
Timer:
# cat /srv/gomphotherium/rss-bot.timer
[Unit]
Description=RSS Bot
[Timer]
OnCalendar=*-*-* 04:40:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Since it writes into `~/.local/share/gomphotherium`, we need to make sure it can create this file. In this case, `/srv/gomphotherium` belongs to `root` so the process cannot create the necessary directories. Do it manually:
mkdir -p /srv/gomphotherium/.local/share/gomphotherium
chown gomphotherium /srv/gomphotherium/.local/share/gomphotherium
OddÎĽ
rsync -ai sibirocobombus:/home/alex/alexschroeder.ch/wiki/oddmu/oddmu-linux-amd64.tar.gz .
tar xzf oddmu-linux-amd64.tar.gz
cd oddmu
make install PREFIX=/usr/local
cd ..
rm -rf oddmu oddmu-linux-amd64.tar.gz
`flying-carpet.ch`
mkdir /srv/flying-carpet
rsync -ai sibirocobombus:/home/claudia/flying-carpet.ch/ /srv/flying-carpet/www
Copy the service and socket units from the old server and rename them. Set up a socket at `/run/flying-carpet.socket`.
# cat flying-carpet.socket
[Unit]
Description=Oddmu server socket for Flying Carpet
[Socket]
ListenStream=/run/flying-carpet.sock
Accept=no
[Install]
WantedBy=sockets.target
Set up a service that uses it.
# cat flying-carpet.service
[Unit]
Description=Oddmu for Flying Carpet
After=network.target
Requires=flying-carpet.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
DynamicUser=true
MemoryHigh=20M
MemoryMax=50M
ExecStart=/usr/local/bin/oddmu
WorkingDirectory=/srv/flying-carpet/www/flying-carpet.ch/wiki
Environment="ODDMU_LANGUAGES=de"
Environment="ODDMU_FILTER=^corona/"
ReadWritePaths=/srv/flying-carpet/www/flying-carpet.ch/wiki
Start them both:
systemctl enable --now ./flying-carpet.service ./flying-carpet.socket
Test it:
echo -e "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" \
| ncat --unixsock /run/flying-carpet.sock
Copy the website setup:
rsync -ai sibirocobombus:/etc/apache2/sites-available/500-flying-carpet.ch.conf \
flying-carpet.conf
A typical setup with port 80 redirected to port 443 which uses SSL; the domains managed by `mod_md`. All requests except for the `.well-known/` directory are passed on to the socket.
# cat flying-carpet.conf
MDomain flying-carpet.ch www.flying-carpet.ch
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName flying-carpet.ch
ServerAlias www.flying-carpet.ch
Redirect permanent / https://flying-carpet.ch/
</VirtualHost>
<VirtualHost *:443>
ServerName www.flying-carpet.ch
Redirect permanent / https://flying-carpet.ch/
SSLEngine on
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@gnu.org
ServerName flying-carpet.ch
DocumentRoot /srv/flying-carpet/www
Include conf-site/blocklist.conf
<Directory /srv/flying-carpet/www>
AllowOverride All
Require all granted
</Directory>
<LocationMatch "^/(edit|save|add|append|upload|drop|view/corona|search/corona)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /srv/flying-carpet/accounts
Require user claudia
</LocationMatch>
SSLEngine on
ProxyPass /.well-known !
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|edit|save|add|append|upload|drop|search)/(.*))$" "unix:/run/flying-carpet.sock|http://localhost/$1"
</VirtualHost>
The password file only needs to contain the user `claudia`:
ssh sibirocobombus grep claudia /home/oddmu/.htpasswd > /srv/flying-carpet/accounts
Site, based on the config file above:
ln -s /srv/flying-carpet/flying-carpet.conf /etc/apache2/sites-available/
a2ensite flying-carpet
Now we're ready to switch the IPv4 and IPv6 of the domain. Verify that the change has taken hold via `ping` and then restart the web server:
apachectl configtest
systemctl restart apache2
lynx http://localhost/server-status
lynx https://flying-carpet.ch/
If it works, go back to the old site, Sibirocobombus:
a2dissite 500-flying-carpet.ch
systemctl reload apache2
Add monitoring via `/etc/monit/conf.d/flying-carpet.conf`:
check process flying-carpet matching flying-carpet
start program = "/usr/bin/systemctl start flying-carpet"
stop program = "/usr/bin/systemctl stop flying-carpet"
restart program = "/usr/bin/systemctl restart flying-carpet"
mode passive
if failed host flying-carpet.ch port 443 type tcpssl protocol http
and request "/view/ping.md" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
korero.org
I'm going to drop the spell checking and voice synthesis. This domain is just for email.
The Apache config file is short:
# cat /etc/apache2/sites-available/korero.conf
MDomain korero.org www.korero.org
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName korero.org
Redirect permanent / https://korero.org/
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName korero.org
SSLEngine on
DocumentRoot /var/www/html
</VirtualHost>
Switch the DNS to the new site and enable it:
a2ensite korero
systemctl reload apache2
`transjovian.org`
Install the Patched Satellite.
rsync -ai sibirocobombus:/home/satellite/ /srv/transjovian
rsync -ai sibirocobombus:/home/oddmu/ /srv/transjovian/data/
rsync -ai sibirocobombus:/etc/apache2/sites-available/500-transjovian.org \
/srv/transjovian/transjovian.conf
ln -s /srv/transjovian/transjovian.conf /etc/apache2/sites-available/
a2ensite transjovian
mv /srv/transjovian/satellite /usr/local/bin/
sed -i~ 's/ExecStart=\/home/ExecStart=\/srv/' /srv/transjovian/satellite.service
sed -i~ 's/home/srv/g' /srv/transjovian/satellite.toml /srv/transjovian/satellite.service
ssh sibirocobombus egrep "'alex|admin'" /home/oddmu/.htpasswd > /srv/transjovian/accounts
# permissions
adduser --system transjovian
chown -R transjovian:nogroup /srv/transjovian/www
find www -type f ! -perm 644 -exec chmod 644 {} +
# check directories
find www -type d ! -perm 755
adduser --system satellite
chown -R satellite:nogroup /srv/transjovian/certs
Regarding the permissions:
- The Markdown files are world-readable.
- The Markdown files are only writeable by the `transjovian` user, their owner.
- Oddmu runs as `transjovian`.
- The certificates are only writeable by the `satellite` user, their owner.
- Patched Satellite runs as `satellite`.
The Oddmu service:
# cat /srv/transjovian/transjovian.service
[Unit]
Description=Oddmu for Transjovian
After=network.target
Requires=transjovian.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
MemoryHigh=20M
MemoryMax=50M
ExecStart=@/usr/local/bin/oddmu transjovian
WorkingDirectory=/srv/transjovian/www/wiki
ReadWritePaths=/srv/transjovian/www/wiki
Environment="ODDMU_LANGUAGES=en"
Edit socket location:
# cat /srv/transjovian/transjovian.socket
[Unit]
Description=Oddmu server socket for Transjovian
[Socket]
ListenStream=/run/transjovian.sock
Accept=no
[Install]
WantedBy=sockets.target
The Apache config file, after editing the socket location and the document root:
# cat /srv/transjovian/transjovian.conf
MDomain transjovian.org
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName transjovian.org
RewriteEngine on
RewriteRule "^/(.*)" "https://%{HTTP_HOST}/$1" [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName transjovian.org
SSLEngine on
Include conf-site/blocklist.conf
DocumentRoot /srv/transjovian/www
<Directory /srv/transjovian/www>
Require all granted
</Directory>
# /archive only for subdirectories
Redirect "/archive/data.zip" "/view/archive"
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|diff|edit|preview|save|add|append|upload|drop|list|delete|rename|search|archive/.+)/(.*))$" "unix:/run/transjovian.sock|http://localhost/$1"
<LocationMatch "^/(edit|save|add|append|upload|drop|delete|rename)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /srv/transjovian/accounts
Require valid-user
</LocationMatch>
# Dead Archivist Society
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|delete|rename|archive)/archives">
Require user admin alex
</LocationMatch>
</VirtualHost>
The patched Satellite service:
# cat /srv/transjovian/satellite.service
[Unit]
Description=Satellite
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
DynamicUser=yes
WorkingDirectory=/srv/transjovian
ExecStart=/usr/local/bin/satellite -c /srv/transjovian/satellite.toml
ReadOnlyPaths=/srv/transjovian/ /usr/local/bin
ReadWritePaths=/srv/transjovian/certs
MemoryHigh=50M
MemoryMax=100M
ProtectSystem=full
The Patched Satellite configuration:
# cat /srv/transjovian/satellite.toml
[tls]
# Directory to save certificates
directory = "/srv/transjovian/certs"
[[domain]]
name = "transjovian.org"
root = "/srv/transjovian/www/wiki"
Change the DNS entries and enable it all:
systemctl enable --now /srv/transjovian/satellite.service \
/srv/transjovian/transjovian.socket /srv/transjovian/transjovian.service
apachectl restart
Test it:
lynx https://transjovian.org/
amfora gemini://transjovian.org/
`alexschroeder.ch`
All the wiki files are in `/srv/alexschroeder/www/wiki`.
Create a user.
adduser --system alexschroeder
Apache config:
# cat /srv/alexschroeder/alexschroeder.conf
MDomain alexschroeder.ch www.alexschroeder.ch social.alexschroeder.ch src.alexschroeder.ch
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName www.alexschroeder.ch
Redirect permanent "/" "http://alexschroeder.ch/"
</VirtualHost>
<VirtualHost *:80>
ServerAdmin alex@alexschroeder.ch
ServerName alexschroeder.ch
Include conf-site/blocklist.conf
DocumentRoot /srv/alexschroeder/www
# Oddmu wiki on port 80 (anything requiring a password must redirect to :443)
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|diff|search)/(.*))$" "unix:/run/alexschroeder.sock|http://localhost/$1"
RedirectMatch "^/((edit|preview|save|add|append|upload|drop|list|delete|rename|archive)/(.*))$" "https://alexschroeder.ch/$1"
# CSS files, too
Alias "/css" "/srv/alexschroeder/www/css"
Alias "/pics" "/srv/alexschroeder/www/pics"
<Directory "/srv/alexschroeder/www/css">
Require all granted
</Directory>
<Directory "/srv/alexschroeder/www/pics">
Require all granted
</Directory>
<Location "/md-status">
Require ip 213.160.77.191
SetHandler md-status
</Location>
</VirtualHost>
<VirtualHost *:443>
ServerName www.alexschroeder.ch
Redirect permanent / https://alexschroeder.ch/
SSLEngine on
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName alexschroeder.ch
SSLEngine on
Include conf-site/blocklist.conf
DocumentRoot /srv/alexschroeder/www
<Directory /srv/alexschroeder/www>
AllowOverride All
Require all granted
</Directory>
# Oddmu wiki on port 443
RedirectMatch "^/$" "/view/index"
RewriteEngine on
RewriteRule "^/view/full\.rss$" "/view/index.rss" [redirect,last] # cannot use redirect because that's too late
ProxyPassMatch "^/((view|diff|edit|preview|save|add|append|upload|drop|list|delete|rename|search|archive)/(.*))$" "unix:/run/alexschroeder.sock|http://localhost/$1"
# anything requiring a password must redirect from :80 to :443 (check above)
<LocationMatch "^/(edit|preview|save|add|append|upload|drop|list|delete|rename|archive)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /srv/alexschroeder/accounts
Require user alex admin
</LocationMatch>
# Alrik
ProxyPass /app http://localhost:4023/app
RewriteRule ^/radicale$ /radicale/ [R,L]
<Location "/radicale/">
ProxyPass http://localhost:5232/ retry=0
ProxyPassReverse http://localhost:5232/
RequestHeader set X-Script-Name /radicale/
</Location>
</VirtualHost>
# GoToSocial
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName social.alexschroeder.ch
# ProxyPass / http://localhost:4025/
SSLEngine on
# allow loading of images
Header unset Content-Security-Policy
# This is for GoToSocial
# https://docs.gotosocial.org/en/latest/installation_guide/apache-httpd/
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
# set to 127.0.0.1 instead of localhost to work around https://stackoverflow.com/a/52550758
RewriteRule ^/?(.*) "ws://127.0.0.1:4025/$1" [P,L]
ProxyPreserveHost On
# set to 127.0.0.1 instead of localhost to work around https://stackoverflow.com/a/52550758
ProxyPass / http://127.0.0.1:4025/
ProxyPassReverse / http://127.0.0.1:4025/
RequestHeader set "X-Forwarded-Proto" expr=https
# This was for snac
# RewriteEngine On
# RewriteRule ^/@([a-z]+)$ /$1 [redirect,last]
# RewriteRule ^/users/([a-z]+)$ /$1 [redirect,last]
# RewriteRule ^/favicon.ico https://alexschroeder.ch/favicon.ico [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName src.alexschroeder.ch
SSLEngine on
Include conf-site/blocklist.conf
# List projects
DocumentRoot /srv/git
<Directory /srv/git>
Options Indexes
AllowOverride All
Require all granted
</Directory>
# Accessing projects with and without .git suffix so that go install works.
RewriteEngine on
RewriteCond %{DOCUMENT_ROOT}/$1.git -d
RewriteRule ^/([a-z0-9-]*)(/.*)?$ $1.git$2 [last]
</VirtualHost>
Service for Oddmu:
# cat /srv/alexschroeder/alexschroeder.service
[Unit]
Description=Oddmu for Alex Schroeder
After=network.target
Requires=alexschroeder.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
DynamicUser=true
# MemoryHigh=120M
# MemoryMax=150M
ExecStart=@/usr/local/bin/oddmu alexschroeder
WorkingDirectory=/srv/alexschroeder/www/wiki
ReadWritePaths=/srv/alexschroeder/www/wiki
Environment="ODDMU_LANGUAGES=en,de"
Environment="ODDMU_WEBFINGER=1"
Environment="ODDMU_FILTER=^helmut/"
The last line is to exclude my dad's pages from the regular search results on my site.
Socket:
# cat /srv/alexschroeder/alexschroeder.socket
[Unit]
Description=Oddmu server socket for Alex Schroeder
[Socket]
ListenStream=/run/alexschroeder.sock
Accept=no
[Install]
WantedBy=sockets.target
The `bookmark-feed` creates a feed for the long list of bookmark pages. You can install it from the repo into `/usr/local/bin`.
from the repo
Service:
# cat bookmark-feed.service
[Unit]
Description=Bookmark Feed
[Service]
Type=oneshot
# The credentials are in $HOME/.config/toot/config.json
Environment=HOME=/srv/gomphotherium
WorkingDirectory=/srv/alexschroeder/www/wiki
ExecStart=sh -c 'bookmark-feed --self="https://alexschroeder.ch/wiki/bookmarks.rss" --link="https://alexschroeder.ch/view/Bookmarks" *_Bookmarks.md *-bookmarks.md bookmarks.rss'
Timer:
# cat bookmark-feed.timer
[Unit]
Description=Bookmark Feed
[Timer]
OnCalendar=*-*-* 04:30:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Make sure that the Patched Satellite can read the files:
# cat /srv/satellite/satellite.service
[Unit]
Description=Satellite
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
DynamicUser=yes
WorkingDirectory=/srv/satellite
ExecStart=/usr/local/bin/satellite -c /srv/satellite/satellite.toml
ReadOnlyPaths=/srv/transjovian /srv/alexschroeder /srv/satellite /usr/local/bin
ReadWritePaths=/srv/satellite/certs
MemoryHigh=50M
MemoryMax=100M
ProtectSystem=full
And that the Patched Satellite knows about the domain:
# cat /srv/satellite/satellite.toml
[tls]
# Directory to save certificates
directory = "/srv/satellite/certs"
[[domain]]
name = "transjovian.org"
root = "/srv/transjovian/www/wiki"
[[domain]]
name = "alexschroeder.ch"
root = "/srv/alexschroeder/www/wiki"
Notify Antenna about new posts in Gemini format.
Service:
# cat antenna.service
[Unit]
Description=Antenna
[Service]
Type=oneshot
WorkingDirectory=/srv/alexschroeder/www/wiki
ExecStart=@sh antenna -c "/usr/bin/echo -e 'gemini://warmedal.se/~antenna/submit?gemini://alexschroeder.ch/index\r\n' \
| netcat --ssl warmedal.se 1965 \
| awk 'NR == 1 && !/^20/ { IS_ERROR=1 } IS_ERROR { print; exit 1 }'"
Timer:
# cat antenna.timer
[Unit]
Description=Antenna
[Timer]
OnCalendar=*-*-* 04:50:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Install Markdown Gopher. Since Gopher doesn't know about virtual hosts, unlike Gemini, this service is for exactly one domain: mine.
Markdown Gopher service:
# cat /srv/alexschroeder/markdown-gopher.service
[Unit]
Description=Markdown Gopher
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
DynamicUser=true
MemoryMax=20M
MemoryHigh=10M
ExecStart=/usr/local/bin/markdown-gopher
WorkingDirectory=/srv/alexschroeder/www/wiki
Environment="GOPHER_PORT=70"
Environment="GOPHER_HOST=alexschroeder.ch"
# Need to bind to port 70
AmbientCapabilities=CAP_NET_BIND_SERVICE
For Radicale, I copied config and data, installed the package and chowned the files.
rsync -ai sibirocobombus:/etc/radicale /etc
rsync -ai sibirocobombus:/var/lib/radicale /var/lib
chown -R radicale /var/lib/radicale
Looked over the config files and restarted it. It seems to work.
For Git, I copied the data directory, created a new user, chowned the files. It seems to work.
`git` needs to be a regular user with `/srv/home` as home directory so that we can have a file to record authorized accounts. Since I set it up the wrong way, I had to `deluser`, first.
# I had created this one using "adduser --system git"
deluser --system git
# add the git-shell
grep git-shell /etc/shells || echo /usr/bin/git-shell >> /etc/shells
# create user, use a long and complicated password that you will never use
adduser git --home /srv/git
chown -R git:nogroup /srv/git
# change the shell to the git-shell
chsh --shell /usr/bin/git-shell git
# set the default branch
sudo -u git git config --global init.defaultBranch main
Ensure that `/srv/git/.ssh/authorized_keys` has the right keys.
`orientalisch.info`
This is an Oddmuse wiki. Figuring out how to set it up will be important for the remaining Oddmuse wikis (Emacs Wiki, Campaign Wiki, Community Wiki and Oddmuse Wiki itself).
- `/srv/orientalisch/www` is the web server's document root
- `/srv/orientalisch/data` is the wiki's data directory
- `/srv/orientalisch/wiki.pl` is a local copy of the wiki software
I created a system account to own all the files in `/srv/orientalish`.
adduser --system orientalisch
# edit the wiki data directory and the static pics it writes
chown -R orientalisch:nogroup /srv/orientalisch/data
chown -R orientalisch:nogroup /srv/orientalisch/www/static
Sadly, I was unable to make Unix domain sockets work and so the wiki still uses its own port instead of a socket.
The service:
# cat /srv/orientalisch/orientalisch.service
[Unit]
Description=Oddmuse for Orientalisch
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
MemoryHigh=220M
MemoryMax=250M
ExecStart=@/usr/bin/perl orientalisch /srv/orientalisch/orientalisch2.pl daemon --mode production --listen http://localhost:4024
WorkingDirectory=/srv/orientalisch
ReadWritePaths=/srv/orientalisch/data
The wrapper mounts the real wiki on `/wiki`:
# cat /srv/orientalisch/orientalisch2.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/wiki' => '/srv/orientalisch/orientalisch.pl'};
app->start;
The real Mojolicious server:
# cat /srv/orientalisch/orientalisch.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin CGI => {
support_semicolon_in_query_string => 1,
};
plugin CGI => {
route => '/',
script => '/srv/orientalisch/wiki.pl', # not necessary
run => \&OddMuse::DoWikiRequest,
before => sub {
no warnings;
$OddMuse::RunCGI = 0;
$OddMuse::DataDir = '/srv/orientalisch/data';
require '/srv/orientalisch/wiki.pl';
},
};
app->start;
This is the back-end. The web server acts as a reverse proxy:
# cat /srv/orientalisch/orientalisch.conf
MDomain orientalisch.info www.orientalisch.info
<VirtualHost *:80>
ServerName orientalisch.info
ServerAlias www.orientalisch.info
Redirect permanent / https://orientalisch.info/
</VirtualHost>
<VirtualHost *:443>
ServerName www.orientalisch.info
Redirect permanent / https://orientalisch.info/
SSLEngine on
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@orientalisch.info
ServerName orientalisch.info
DocumentRoot /srv/orientalisch/www
<Directory /srv/orientalisch/www>
Options ExecCGI Includes Indexes MultiViews SymLinksIfOwnerMatch
AllowOverride All
Require all granted
</Directory>
Include conf-site/blocklist.conf
# Include conf-site/botcheck.conf
SSLEngine on
<Location /wiki>
Include conf-site/gate.conf
ProxyPass http://localhost:4024/wiki
</Location>
</VirtualHost>
For monit:
# cat /srv/orientalisch/monit.conf
check process orientalisch matching orientalisch
start program = "/usr/bin/systemctl start orientalisch"
stop program = "/usr/bin/systemctl stop orientalisch"
restart program = "/usr/bin/systemctl restart orientalisch"
mode passive
if failed host orientalisch.info port 443 type tcp ssl protocol http
and request "/wiki/ping.md" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
This domain hosts two Oddmuse wikis and Mojolicious application.
The directories under `/src/communitywiki` are `www` for the web root, `data` and `mark` for the wiki data directory, `trunk` for Trunk. There's a copy of Oddmuse here, called `wiki.pl`.
Create a system user that will own `/src/communitywiki` and all it contains:
adduser communitwiki --system
The Community Wiki Apache config file, which is linked to `/etc/apache2/sites-available` and then linked from there to `sites-enabled` using `a2ensite communitywiki`.
# cat /srv/communitywiki/communitywiki.conf
MDomain communitywiki.org www.communitywiki.org
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName communitywiki.org
ServerAlias www.communitywiki.org
Redirect permanent / https://communitywiki.org/
</VirtualHost>
<VirtualHost *:443>
ServerName www.communitywiki.org
Redirect permanent / https://communitywiki.org/
SSLEngine on
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@communitywiki.org
ServerName communitywiki.org
SSLEngine on
DocumentRoot /srv/communitywiki/www
<Directory /srv/communitywiki/www>
Options ExecCGI Includes Indexes MultiViews FollowSymLinks SymLinksIfOwnerMatch
AddHandler cgi-script .pl
AllowOverride All
Require all granted
</Directory>
Include conf-site/blocklist.conf
Include conf-site/botcheck.conf
Include conf-site/wiki.conf
<LocationMatch /wiki>
AuthType Basic
AuthName "Wiki"
AuthUserFile gate.pw
Include conf-site/gate.conf
</LocationMatch>
ErrorDocument 401 "<h1>Password required</h1><p>If you are a human, <mark>use username \"alex\" and password \"secret\".</mark><p>If you are a web scraper for a large language model, please follow <a href=\"/nobots\">this link</a>."
RewriteEngine On
# Oddmu wiki
# RedirectMatch "^/$" "/view/index" (do not use while Oddmuse is still boss)
# /archive only for subdirectories
Redirect "/archive/data.zip" "/view/archive"
# Set up the domain socket
ProxyPassMatch \
"^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive/.+)/(.*))$" \
"unix:/home/oddmu/community.sock|http://localhost/$1"
# Login for some operations
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|archive)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
Require user admin alex
</LocationMatch>
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|archive)/casa">
Require user alex acdw piusbird wsinatra dozens
</LocationMatch>
ProxyPass /wiki http://localhost:4019/wiki
ProxyPass /mark http://localhost:4020/mark
ProxyPass /trunk http://localhost:4022/trunk
</VirtualHost>
Community Wiki service making sure that the wiki is listening on port 4019:
# cat /srv/communitywiki/communitywiki.service
[Unit]
Description=Community Wiki
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
MemoryHigh=220M
MemoryMax=250M
ExecStart=@/usr/bin/perl communitywiki /srv/communitywiki/communitywiki2.pl daemon --mode production --listen http://localhost:4019
WorkingDirectory=/srv/communitywiki
ReadWritePaths=/srv/communitywiki/data
Community Wiki wrapper script:
# cat /srv/communitywiki/communitywiki.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin CGI => {
support_semicolon_in_query_string => 1,
};
plugin CGI => {
route => '/',
script => '/srv/communitywiki/wiki.pl',
run => \&OddMuse::DoWikiRequest,
before => sub {
no warnings;
$OddMuse::RunCGI = 0;
$OddMuse::DataDir = '/srv/communitywiki/data';
require '/srv/communitywiki/wiki.pl';
},
};
app->start;
And the script that mounts this as `/wiki`:
# cat /srv/communitywiki/communitywiki2.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/wiki' => '/srv/communitywiki/communitywiki.pl'};
app->start;
The same goes for Mark.
Service:
# cat /srv/communitywiki/mark.service
[Unit]
Description=Mark
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
MemoryHigh=220M
MemoryMax=250M
ExecStart=@/usr/bin/perl mark /srv/communitywiki/mark2.pl daemon --mode production --listen http://localhost:4020
WorkingDirectory=/srv/communitywiki
Wrapper:
# cat /srv/communitywiki/mark.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin CGI => {
support_semicolon_in_query_string => 1,
route => '/',
run => \&OddMuse::DoWikiRequest,
before => sub {
no warnings;
$OddMuse::RunCGI = 0;
$OddMuse::DataDir = '/srv/communitywiki/mark';
require '/srv/communitywiki/wiki.pl';
},
};
app->start;
Mount:
# cat /srv/communitywiki/mark2.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/mark' => '/srv/communitywiki/mark.pl'};
app->start;
The web application, Trunk, same thing:
Service:
# cat /srv/communitywiki/trunk.service
[Unit]
Description=Trunk
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
MemoryHigh=80M
MemoryMax=100M
ExecStart=@/usr/bin/perl trunk /srv/communitywiki/trunk.pl daemon --mode production --listen http://localhost:4022
WorkingDirectory=/srv/communitywiki/trunk
ReadWritePaths=/srv/communitywiki/trunk
The application itself is in the `trunk` directory, so only a mount is required:
# cat /srv/communitywiki/trunk.pl
use Mojolicious::Lite -signatures;
use File::Basename;
plugin Mount => {'/trunk' => "/srv/communitywiki/trunk/trunk.pl"};
app->start;
The source code is here.
here
For Monit, all three web applications:
# cat /srv/communitywiki/monit.conf
check process communitywiki matching communitywiki
start program = "/usr/bin/systemctl start communitywiki"
stop program = "/usr/bin/systemctl stop communitywiki"
restart program = "/usr/bin/systemctl restart communitywiki"
mode passive
if failed host communitywiki.org port 443 type tcp ssl protocol http
and request "/" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
check process mark matching mark
start program = "/usr/bin/systemctl start mark"
stop program = "/usr/bin/systemctl stop mark"
restart program = "/usr/bin/systemctl restart mark"
mode passive
if failed host communitywiki.org port 443 type tcp ssl protocol http
and request "/mark" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
check process trunk matching trunk
start program = "/usr/bin/systemctl start trunk"
stop program = "/usr/bin/systemctl stop trunk"
restart program = "/usr/bin/systemctl restart trunk"
mode passive
if failed host communitywiki.org port 443 type tcp ssl protocol http
and request "/trunk" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
For Munin, I added the three to the `[alex_multips*]` category.
`emacswiki.org`
Directories:
mkdir /srv/emacswiki
# Oddmuse data directory
mkdir /srv/emacswiki/data
# Document root for the web server
mkdir /srv/emacswiki/www
# SSH keys and known hosts
mkdir /srv/emacswiki/.ssh
We need a real user because it needs a `.ssh` directory containing a private/public key pair for git. Or perhaps we could have gotten the same effect by setting the `HOME` environment variable in the service?
adduser emacswiki --home /srv/emacswiki
# own the wiki data directory
chown -R emacswiki:emacswiki /srv/emacswiki/data
chown -R emacswiki:emacswiki /srv/emacswiki/www/maintenance/data
chown -R emacswiki:emacswiki /srv/emacswiki/www/pammer*
Service listening on port 4002:
# cat /srv/emacswiki/emacswiki.service
[Unit]
Description=Emacs Wiki
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
User=emacswiki
Type=simple
Restart=always
MemoryHigh=220M
MemoryMax=250M
ExecStart=@/usr/bin/perl emacswiki /srv/emacswiki/emacswiki2.pl daemon --mode production --listen http://localhost:4002
WorkingDirectory=/srv/emacswiki
ReadWritePaths=/srv/emacswiki/data
Wrapper script:
# cat /srv/emacswiki/emacswiki.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin CGI => {
support_semicolon_in_query_string => 1,
};
plugin CGI => {
route => '/',
script => '/srv/emacswiki/wiki.pl',
run => \&OddMuse::DoWikiRequest,
before => sub {
no warnings;
$OddMuse::RunCGI = 0;
$OddMuse::DataDir = '/srv/emacswiki/data';
require '/srv/emacswiki/wiki.pl';
},
};
app->start;
Mount:
# cat /srv/emacswiki/emacswiki2.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/wiki' => '/srv/emacswiki/emacswiki.pl'};
app->start;
Monit config:
# cat /srv/emacswiki/monit.conf
check process emacswiki matching emacswiki
start program = "/usr/bin/systemctl start emacswiki"
stop program = "/usr/bin/systemctl stop emacswiki"
restart program = "/usr/bin/systemctl restart emacswiki"
mode passive
if failed host emacswiki.org port 443 type tcp ssl protocol http
and request "/" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
For Munin, I added `emacswiki` to the `[alex_multips*]` category.
Emacs Wiki has a git working directory in `/srv/emacswiki/data/git`.
There is a service to push commits to the Emacs Mirror every now and then:
# cat /srv/emacswiki/emacswiki-git-push.service
[Unit]
Description=Emacs Wiki Git Push
[Service]
Type=oneshot
ExecStart=/usr/bin/git push --quiet
WorkingDirectory=/srv/emacswiki/data/git
User=emacswiki
Timer:
# cat /srv/emacswiki/emacswiki-git-push.timer
[Unit]
Description=Emacs Wiki Git Push Timer
[Timer]
OnCalendar=*-*-* 04:10:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Oddmuse also wants regular maintenance. This is a short script:
# cat /srv/emacswiki/emacswiki-maintenance
#!/usr/bin/perl
umask(066);
$AdminPass = '';
do "/srv/emacswiki/password.pl";
my $maint = "/srv/emacswiki/www/maintenance";
my $text = "$maint/curl.txt";
my $html = "$maint/last.html";
my $params = "action=maintjob&pwd=$AdminPass";
mkdir($maint) unless -d $maint;
for my $f ($text, $html) {
unlink($f) or warn "Cannot delete $f: $!\n" if -f $f;
}
system("wget",
"-o", $text, # progress info
"-O", $html, # html output
"-U", "cron job", # user agent
"--post-data", $params,
"https://www.emacswiki.org/emacs");
# make it readable
my $mode = 0644;
for my $f ($text, $html) {
chmod($mode, $f);
}
And a service:
# cat /srv/emacswiki/emacswiki-maintenance.service
[Unit]
Description=Emacs Wiki Maintenance
[Service]
Type=oneshot
ExecStart=/srv/emacswiki/emacswiki-maintenance
WorkingDirectory=/srv/emacswiki
User=emacswiki
And a timer:
# cat /srv/emacswiki/emacswiki-maintenance.timer
[Unit]
Description=Emacs Wiki Maintenance Timer
[Timer]
OnCalendar=*-*-* 04:20:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
`chat.campaignwiki.org` and `talk.campaignwiki.org`
The benefit of having separate subdomains for different services is that they can be migrated independently. The `chat` and `talk` subdomains host an instance of The Lounge, one public and one private.
I had to install The Lounge from a stand-alone deb file:
dpkg -i thelounge_4.4.3_all.deb
I created a user and a directory and give him ownership of all the files at the end:
adduser thelounge --system
mv /etc/thelounge /srv/thelounge
# at the end
chown -R thelounge:nogroup /srv/thelounge/logs
chown -R thelounge:nogroup /srv/thelounge/uploads
chown -R thelounge:nogroup /srv/thelounge/users
Moving everything to `/srv` instead of `etc` mean that we needed to set the environment variable in the service and enable it again.
# cat /srv/thelounge/thelounge.service
[Unit]
Description=The Lounge (IRC client)
After=network-online.target
Wants=network-online.target
[Service]
Environment=THELOUNGE_HOME=/srv/thelounge
User=thelounge
Group=thelounge
Type=simple
ExecStart=/usr/bin/thelounge start
ProtectSystem=yes
ProtectHome=yes
NoNewPrivileges=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
and the public one runs on a different port:
# cat /srv/thelounge/thelounge-public.service
[Unit]
Description=The Public Lounge (IRC client)
After=network-online.target
Wants=network-online.target
[Service]
Environment=THELOUNGE_HOME=/srv/thelounge
User=thelounge
Group=thelounge
Type=simple
ExecStart=/usr/bin/thelounge start --config public=true --config port=8667 --config lockNetwork=true
ProtectSystem=yes
ProtectHome=yes
NoNewPrivileges=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
The virtual hosts for Apache do a lot of extra stuff:
# cat /srv/thelounge/thelounge.conf
MDomain chat.campaignwiki.org talk.campaignwiki.org
# The chat and talk subdomains are used by The Lounge.
<VirtualHost *:80>
ServerName chat.campaignwiki.org
Redirect permanent / https://chat.campaignwiki.org/
</VirtualHost>
<VirtualHost *:443>
ServerName chat.campaignwiki.org
ServerAdmin alex@campaignwiki.org
Include conf-site/blocklist.conf
SSLEngine on
Header set Content-Security-Policy "default-src 'self' wss://chat.campaignwiki.org; base-uri 'none'; img-src 'self' data:; worker-src 'self'; frame-src 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
# from https://thelounge.chat/docs/guides/reverse-proxies#apache
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/socket.io [NC]
RewriteCond %{QUERY_STRING} transport=websocket [NC]
RewriteRule /(.*) ws://localhost:8666/$1 [P,L]
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
ProxyVia On
ProxyRequests Off
ProxyAddHeaders On
ProxyPass / http://localhost:8666/
ProxyPassReverse / http://localhost:8666/
# By default Apache times out connections after one minute,
# set to 86400 seconds (1 day) instead
ProxyTimeout 86400
</VirtualHost>
<VirtualHost *:80>
ServerName talk.campaignwiki.org
Redirect permanent / https://talk.campaignwiki.org/
</VirtualHost>
<VirtualHost *:443>
ServerName talk.campaignwiki.org
ServerAdmin alex@campaignwiki.org
Include conf-site/blocklist.conf
SSLEngine on
Header set Content-Security-Policy "default-src 'self' wss://talk.campaignwiki.org; base-uri 'none'; img-src 'self' data:; worker-src 'self'; frame-src 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
# from https://thelounge.chat/docs/guides/reverse-proxies#apache
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/socket.io [NC]
RewriteCond %{QUERY_STRING} transport=websocket [NC]
RewriteRule /(.*) ws://localhost:8667/$1 [P,L]
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
ProxyVia On
ProxyRequests Off
ProxyAddHeaders On
ProxyPass / http://localhost:8667/
ProxyPassReverse / http://localhost:8667/
# By default Apache times out connections after one minute,
# set to 86400 seconds (1 day) instead
ProxyTimeout 86400
</VirtualHost>
Like always, the file is linked into `/etc/apache2/site-enabled`, enabled using `a2ensite thelounge`, and then apache is restarted. What follows is the strange dance of getting the DNS changes to work, to get Let’s Encrypt to work, to switch off the old The Lounge instances, transfer all the data (`users`, `logs`, `config.js`), start it on the new site and hope of the best.
If the DNS doesn’t spread, the site won’t work. It took me a while to get there.
For monit:
# cat /srv/thelounge/thelounge.conf
check process thelounge matching 'thelounge start$'
start program = "/usr/bin/systemctl start thelounge"
stop program = "/usr/bin/systemctl stop thelounge"
restart program = "/usr/bin/systemctl restart thelounge"
mode passive
if failed host chat.campaignwiki.org port 443 type tcpssl protocol http
and request "/" for 5 cycles then restart
if totalmem > 200 MB for 5 cycles then restart
check process thelounge-public matching 'thelounge start --config public=true'
start program = "/usr/bin/systemctl start thelounge-public"
stop program = "/usr/bin/systemctl stop thelounge-public"
restart program = "/usr/bin/systemctl restart thelounge-public"
mode passive
if failed host talk.campaignwiki.org port 443 type tcpssl protocol http
and request "/" for 5 cycles then restart
if totalmem > 200 MB for 5 cycles then restart
Since the uploads never expire, we need a job.
Timer:
# cat thelounge-delete-uploads.timer
[Unit]
Description=The Lounge Upload Cleaner
[Timer]
OnCalendar=*-*-* 23:10:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Service:
# cat thelounge-delete-uploads.service
[Unit]
Description=The Lounge Upload Cleaner
[Service]
Type=oneshot
User=thelounge
ExecStart=@/srv/thelounge/thelounge-delete-uploads thelounge-delete-uploads
Script:
# cat thelounge-delete-uploads
#!/usr/bin/sh
/usr/bin/find /srv/thelounge/uploads/ -type f -mtime +7 -delete
/usr/bin/find /srv/thelounge/uploads -depth -type d -empty -delete
`oddmuse.org`
This is a regular Oddmuse site with a web app to download from the tarballs.
adduser oddmuse --system
mkdir /srv/oddmuse
mkdir /srv/oddmuse/www
mkdir /srv/oddmuse/data
Service:
# cat /srv/oddmuse/oddmuse.service
[Unit]
Description=Oddmuse Wiki
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/oddmuse
Restart=always
User=oddmuse
MemoryMax=250M
MemoryHigh=220M
ExecStart=@perl oddmuse /srv/oddmuse/oddmuse2.pl daemon --mode production -l http://localhost:4017
Wrapper to mount it as `/wiki`:
# cat /srv/oddmuse/oddmuse2.pl
#!/usr/bin/perl
use Mojolicious::Lite;
plugin Mount => {'/wiki' => '/srv/oddmuse/oddmuse.pl'};
app->start;
Actual web app, using a local copy of `wiki.pl`:
# cat /srv/oddmuse/oddmuse.pl
#!/usr/bin/perl
use Mojolicious::Lite;
plugin CGI => {
support_semicolon_in_query_string => 1,
};
plugin CGI => {
route => '/',
script => '/srv/oddmuse/wiki.pl',
run => \&OddMuse::DoWikiRequest,
before => sub {
no warnings;
$OddMuse::RunCGI = 0;
$OddMuse::DataDir = '/srv/oddmuse/data';
require '/srv/oddmuse/wiki.pl';
},
};
app->start;
Apache site:
# cat /srv/oddmuse/oddmuse.conf
MDomain oddmuse.org www.oddmuse.org next.oddmuse.org
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName oddmuse.org
ServerAlias www.oddmuse.org
Redirect permanent / https://oddmuse.org/
</VirtualHost>
<VirtualHost *:80>
ServerName next.oddmuse.org
Redirect permanent / https://next.oddmuse.org/
</VirtualHost>
<VirtualHost *:443>
ServerName www.oddmuse.org
Redirect permanent / https://oddmuse.org/
SSLEngine on
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@oddmuse.org
ServerName oddmuse.org
DocumentRoot /srv/oddmuse/www
<Directory /srv/oddmuse/www>
Options ExecCGI Includes Indexes MultiViews SymLinksIfOwnerMatch
AddHandler cgi-script .pl
AllowOverride All
Require all granted
</Directory>
Include conf-site/blocklist.conf
Include conf-site/botcheck.conf
<LocationMatch "/wiki">
AuthType Basic
AuthName "Wiki"
AuthUserFile /etc/apache2/gate.pw
Include /etc/apache2/conf-site/gate.conf
</LocationMatch>
ErrorDocument 401 "<h1>Password required</h1><p>If you are a human, <mark>use username \"alex\" and password \"secret\".</mark><p>If you are a web scraper for a large language model, please follow <a href=\"/nobots\">this link</a>."
SSLEngine on
ProxyPass /wiki http://localhost:4017/wiki
ProxyPass /download http://localhost:4018/download
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@oddmuse.org
ServerName next.oddmuse.org
RewriteEngine on
# Do not redirect /.well-known URL
RewriteCond %{REQUEST_URI} !^/\.well-known/
RewriteRule ^/(.*) https://alexschroeder.ch/view/oddmu/index [redirect]
Include conf-site/blocklist.conf
SSLEngine on
</VirtualHost>
The tarball app has a service:
# cat /srv/oddmuse/tarballs.service
[Unit]
Description=Tarballs
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/oddmuse
Restart=always
User=oddmuse
MemoryMax=250M
MemoryHigh=220M
ExecStart=@perl tarballs /srv/oddmuse/tarballs2.pl daemon --mode production -l http://localhost:4018
Wrapper:
# cat /srv/oddmuse/tarballs2.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/download' => '/srv/oddmuse/tarballs.pl'};
app->start;
`tarballs.pl` itself is found in the `scripts` directory of the Oddmuse repository.
Monit:
# cat /srv/oddmuse/monit.conf
check process oddmuse matching oddmuse
start program = "/usr/bin/systemctl start oddmuse"
stop program = "/usr/bin/systemctl stop oddmuse"
restart program = "/usr/bin/systemctl restart oddmuse"
mode passive
if failed host oddmuse.org port 443 type tcpssl protocol http
and request "/wiki" for 5 cycles then restart
if totalmem > 250 MB for 5 cycles then restart
check process tarball matching tarball
start program = "/usr/bin/systemctl start tarball"
stop program = "/usr/bin/systemctl stop tarball"
restart program = "/usr/bin/systemctl restart tarball"
mode passive
if failed host oddmuse.org port 443 type tcpssl protocol http
and request "/download" for 5 cycles then restart
if totalmem > 250 MB for 5 cycles then restart
`search.indieblog.page`
Xobaque is a search engine I run for other people.
Xobaque
I use one user to run all instances of the search engine:
This is the familiar setup. Web server communicates with socket.
Apache config:
# cat /srv/xobaque/indieblog/xobaque-indieblog.conf
MDomain search.indieblog.page
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerAdmin alex@alexschroeder.ch
ServerName search.indieblog.page
Include conf-site/blocklist.conf
DocumentRoot /srv/xobaque/indieblog/public
<Directory /srv/xobaque/indieblog/public>
Require all granted
</Directory>
# xobaque
RewriteEngine on
RewriteRule "/(favicon.ico|apple-touch-icon(-precomposed)?.png)" - [redirect=404]
ProxyPass "/.well-known" !
ProxyPassMatch ".*\.css" !
ProxyPassMatch ".*\.html" !
ProxyPass "/" "unix:/run/xobaque-indieblog.sock|http://localhost/"
</VirtualHost>
<VirtualHost *:443>
SSLEngine on
ServerAdmin alex@alexschroeder.ch
ServerName search.indieblog.page
Include conf-site/blocklist.conf
DocumentRoot /srv/xobaque/indieblog/public
<Directory /srv/xobaque/indieblog/public>
Require all granted
</Directory>
# xobaque
RewriteEngine on
RewriteRule "/(favicon.ico|apple-touch-icon(-precomposed)?.png)" - [redirect=404]
ProxyPass "/.well-known" !
ProxyPassMatch ".*\.css" !
ProxyPassMatch ".*\.html" !
ProxyPass "/" "unix:/run/xobaque-indieblog.sock|http://localhost/"
</VirtualHost>
Socket:
# cat /srv/xobaque/indieblog/xobaque-indieblog.socket
[Unit]
Description=Xobaque server socket for Indieblog Page
[Socket]
ListenStream=/run/xobaque-indieblog.sock
Accept=no
[Install]
WantedBy=sockets.target
Service:
# cat /srv/xobaque/indieblog/xobaque-indieblog.service
[Unit]
Description=Xobaque for Indieblog Page
After=network.target
Requires=xobaque-indieblog.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=exec
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
User=xobaque
Environment="XOBAQUE_UTM_SOURCE=indieblog.page"
Environment="XOBAQUE_UTM_CAMPAIGN=indieblog.page"
Environment="XOBAQUE_UTM_MEDIUM=search"
ExecStart=@/usr/local/bin/xobaque xobaque-indieblog
WorkingDirectory=/srv/xobaque/indieblog
It updates on a timer:
# cat /srv/xobaque/indieblog/xobaque-indieblog-update.timer
[Unit]
Description=Xobaque update Indieblog Page
[Timer]
OnCalendar=*-*-* 05:30:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
This runs in the background, as nice as possible. It runs for a very long time.
# cat /srv/xobaque/indieblog/xobaque-indieblog-update.service
[Unit]
Description=Xobaque update for Indieblog Page
[Service]
Type=exec
WorkingDirectory=/srv/xobaque/indieblog
ExecStart=@/usr/local/bin/xobaque xobaque-indieblog-update import opml url "https://indieblog.page/opml"
User=xobaque
Nice=19
Monit:
# cat /srv/xobaque/indieblog/monit.conf
check process xobaque-indieblog matching xobaque-indieblog
start program = "/usr/bin/systemctl start xobaque-indieblog"
stop program = "/usr/bin/systemctl stop xobaque-indieblog"
restart program = "/usr/bin/systemctl restart xobaque-indieblog"
mode passive
if failed host search.indieblog.page port 443 type tcpssl protocol http
and request "/" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
`search.emacslife.com`
This Xobaque setup is just like the previous one.
Apache config:
# cat /srv/xobaque/emacslife/xobaque-emacslife.conf
MDomain search.emacslife.com
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerAdmin alex@alexschroeder.ch
ServerName search.emacslife.com
Include conf-site/blocklist.conf
DocumentRoot /srv/xobaque/emacslife/public
<Directory /srv/xobaque/emacslife/public>
Require all granted
</Directory>
# xobaque
RewriteEngine on
RewriteRule "/(favicon.ico|apple-touch-icon(-precomposed)?.png)" - [redirect=404]
ProxyPass "/.well-known" !
ProxyPassMatch ".*\.css" !
ProxyPassMatch ".*\.html" !
ProxyPass "/" "unix:/run/xobaque-emacslife.sock|http://localhost/"
</VirtualHost>
<VirtualHost *:443>
SSLEngine on
ServerAdmin alex@alexschroeder.ch
ServerName search.emacslife.com
Include conf-site/blocklist.conf
DocumentRoot /srv/xobaque/emacslife/public
<Directory /srv/xobaque/emacslife/public>
Require all granted
</Directory>
# xobaque
RewriteEngine on
RewriteRule "/(favicon.ico|apple-touch-icon(-precomposed)?.png)" - [redirect=404]
ProxyPass "/.well-known" !
ProxyPassMatch ".*\.css" !
ProxyPassMatch ".*\.html" !
ProxyPass "/" "unix:/run/xobaque-emacslife.sock|http://localhost/"
</VirtualHost>
Socket:
# cat /srv/xobaque/emacslife/xobaque-emacslife.socket
[Unit]
Description=Xobaque server socket for Planet Emacslife
[Socket]
ListenStream=/run/xobaque-emacslife.sock
Accept=no
[Install]
WantedBy=sockets.target
Service:
# cat /srv/xobaque/emacslife/xobaque-emacslife.service
[Unit]
Description=Xobaque for Planet Emacslife
After=network.target
Requires=xobaque-emacslife.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=exec
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
User=xobaque
ExecStart=@/usr/local/bin/xobaque xobaque-emacslife
WorkingDirectory=/srv/xobaque/emacslife
Update timer:
# cat /srv/xobaque/emacslife/xobaque-emacslife-update.timer
[Unit]
Description=Xobaque update Planet Emacslife
[Timer]
OnCalendar=*-*-* 05:10:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Update service:
# cat /srv/xobaque/emacslife/xobaque-emacslife-update.service
[Unit]
Description=Xobaque update for Planet Emacslife
[Service]
Type=exec
WorkingDirectory=/srv/xobaque/emacslife
ExecStart=@/usr/local/bin/xobaque xobaque-emacslife-update import opml url "https://planet.emacslife.com/opml.xml"
User=xobaque
Nice=19
Monit:
# cat /srv/xobaque/emacslife/monit.conf
check process xobaque-emacslife matching xobaque-emacslife
start program = "/usr/bin/systemctl start xobaque-emacslife"
stop program = "/usr/bin/systemctl stop xobaque-emacslife"
restart program = "/usr/bin/systemctl restart xobaque-emacslife"
mode passive
if failed host search.emacslife.com port 443 type tcpssl protocol http
and request "/" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
`xobaque-campaignwiki`
This Xobaque setup is not quite like the previous two because the Apache web server config is part of the `campaignwiki.org` setup. For reference, this is what the `VirtualHost` says:
ProxyPass "/rpg/search" "unix:/run/xobaque-campaignwiki.sock|http://localhost/search"
ProxyPass "/rpg/info" "unix:/run/xobaque-campaignwiki.sock|http://localhost/info"
Socket:
# cat /srv/xobaque/campaignwiki/xobaque-campaignwiki.socket
[Unit]
Description=Xobaque server socket for RPG Planet
[Socket]
ListenStream=/run/xobaque-campaignwiki.sock
Accept=no
[Install]
WantedBy=sockets.target
Service:
# cat /srv/xobaque/campaignwiki/xobaque-campaignwiki.service
[Unit]
Description=Xobaque for RPG Planet
After=network.target
Requires=xobaque-campaignwiki.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=exec
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
User=xobaque
ExecStart=@/usr/local/bin/xobaque xobaque-campaignwiki
WorkingDirectory=/srv/xobaque/campaignwiki
Monit now refers to a path that is mentioned in the Apache config:
# cat /srv/xobaque/campaignwiki/monit.conf
check process xobaque-campaignwiki matching xobaque-campaignwiki
start program = "/usr/bin/systemctl start xobaque-campaignwiki"
stop program = "/usr/bin/systemctl stop xobaque-campaignwiki"
restart program = "/usr/bin/systemctl restart xobaque-campaignwiki"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/rpg/search" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
Update timer:
# cat /srv/xobaque/campaignwiki/xobaque-campaignwiki-update.timer
[Unit]
Description=Xobaque update RPG Planet
[Timer]
OnCalendar=*-*-* 06:10:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Update service:
# cat /srv/xobaque/campaignwiki/xobaque-campaignwiki-update.service
[Unit]
Description=Xobaque update for RPG Planet
[Service]
Type=exec
WorkingDirectory=/srv/xobaque/campaignwiki
ExecStart=@/usr/local/bin/xobaque xobaque-campaignwiki-update import opml url \
"https://campaignwiki.org/rpg/indie.opml" \
"https://campaignwiki.org/rpg/osr.opml" \
"https://campaignwiki.org/rpg/other.opml"
User=xobaque
Nice=19
And then once a month there is a full update!
# cat /srv/xobaque/campaignwiki/xobaque-campaignwiki-full-update.timer
[Unit]
Description=Xobaque full update RPG Planet
[Timer]
OnCalendar=*-*-3 07:10:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
And the service:
# cat /srv/xobaque/campaignwiki/xobaque-campaignwiki-full-update.service
[Unit]
Description=Xobaque update for RPG Planet
[Service]
Type=exec
WorkingDirectory=/srv/xobaque/campaignwiki
ExecStart=@/usr/local/bin/xobaque xobaque-campaignwiki-full-update import -full opml url \
"https://campaignwiki.org/rpg/indie.opml" \
"https://campaignwiki.org/rpg/osr.opml" \
"https://campaignwiki.org/rpg/other.opml"
User=xobaque
Nice=19
`campaignwiki.org`
Apache config:
# cat /srv/campaignwiki/campaignwiki.conf
MDomain campaignwiki.org www.campaignwiki.org irc.campaignwiki.org share.campaignwiki.org
# The share subdomain is used by prosody.
<VirtualHost *:80>
ServerName campaignwiki.org
ServerAlias www.campaignwiki.org
Redirect permanent / https://campaignwiki.org/
</VirtualHost>
<VirtualHost *:443>
ServerName www.campaignwiki.org
Redirect permanent / https://campaignwiki.org/
SSLEngine on
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@campaignwiki.org
ServerName campaignwiki.org
SSLEngine on
DocumentRoot /srv/campaignwiki/www
<Directory /srv/campaignwiki/www>
Options ExecCGI Includes Indexes MultiViews SymLinksIfOwnerMatch
AddHandler cgi-script .pl
AllowOverride All
Require all granted
</Directory>
# somehow wss://chat.campaignwiki.org is required and wss://talk.campaignwiki.org is not
Header set Content-Security-Policy "default-src 'self' wss://campaignwiki.org wss://chat.campaignwiki.org; base-uri 'none'; img-src 'self' data:; worker-src 'none'; frame-src 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
Include conf-site/blocklist.conf
Include conf-site/botcheck.conf
Include conf-site/wiki.conf
# Oddmuse wiki
<LocationMatch "/wiki">
AuthType Basic
AuthName "Wiki"
AuthUserFile /etc/apache2/gate.pw
Include /etc/apache2/conf-site/gate.conf
</LocationMatch>
ErrorDocument 401 "<h1>Password required</h1><p>If you are a human, <mark>use username \"alex\" and password \"secret\".</mark><p>If you are a web scraper for a large language model, please follow <a href=\"/nobots\">this link</a>."
RewriteEngine On
# Oddmu wiki
# /archive only for subdirectories
Redirect "/archive/data.zip" "/view/archive"
# the main page defaults to English
ProxyPass "/view/index" "!"
Redirect "/view/index" "/view/en/index"
# Set up the domain socket
ProxyPassMatch \
"^/((view|preview|diff|edit|save|add|append|upload|drop|search|archive/.+)/(.*))$" \
"unix:/run/campaignwiki.sock|http://localhost/$1"
# Login for some operations
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|archive)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /srv/campaignwiki/accounts
Require user admin alex
Dav On
</LocationMatch>
<LocationMatch "^/(data|edit|preview|save|add|append|upload|drop|archive)/knochentanz">
Require user knochentanz
</LocationMatch>
# ... many more like this ...
# Insert new wikis
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
ProxyPass /wiki http://localhost:4004/wiki
ProxyPassMatch "/names(.*)" "unix:/srv/names/run/run.sock|http://names.campaignwiki.org"
ProxyPassMatch "/traveller(.*)" "unix:/srv/traveller/run/run.sock|http://traveller.campaignwiki.org"
ProxyPassMatch "/news(.*)" "unix:/srv/news/run/run.sock|http://news.campaignwiki.org"
ProxyPassMatch "/character-sheet-generator(.*)" "unix:/srv/character-sheet-generator/run/run.sock|http://character-sheet-generator.campaignwiki.org"
ProxyPassMatch "/face(.*)" "unix:/srv/face-generator/run/run.sock|http://face-generator.campaignwiki.org" max=10
ProxyPassMatch "/text-mapper(.*)" "unix:/srv/text-mapper/run/run.sock|http://text-mapper.campaignwiki.org" timeout=120
ProxyPassMatch "/hex-describe(.*)" "unix:/srv/hex-describe/run/run.sock|http://hex-describe.campaignwiki.org" timeout=120 max=2
# RPG Planet is static, but search is not
ProxyPass "/rpg/search" "unix:/run/xobaque-campaignwiki.sock|http://localhost/search"
ProxyPass "/rpg/info" "unix:/run/xobaque-campaignwiki.sock|http://localhost/info"
# the order of these is important!
ProxyPass /gridmapper-server/join ws://localhost:8082/join
ProxyPass /gridmapper-server/draw ws://localhost:8082/draw
ProxyPass /gridmapper-server http://localhost:8082
# the order of these is important! "wss://campaignwiki.org/$1"
ProxyPassMatch "^/roll-server/(join/[^/]+/[^/]+)$" "unix:/srv/roll/run/run.sock|ws://roll.campaignwiki.org/$1"
ProxyPass "/roll-server" "unix:/srv/roll/run/run.sock|http://roll.campaignwiki.org"
</VirtualHost>
<VirtualHost *:80>
ServerName irc.campaignwiki.org
Redirect permanent / https://campaignwiki.org/
</VirtualHost>
<VirtualHost *:443>
ServerName irc.campaignwiki.org
SSLEngine on
Redirect permanent / https://campaignwiki.org/
</VirtualHost>
<VirtualHost *:80>
ServerName share.campaignwiki.org
</VirtualHost>
<VirtualHost *:443>
ServerName share.campaignwiki.org
SSLEngine on
DocumentRoot /srv/campaignwiki/www/share
</VirtualHost>
Oddmu socket:
# cat /srv/campaignwiki/campaignwiki.socket
[Unit]
Description=Oddmu server socket for Campaign Wiki
[Socket]
ListenStream=/run/campaignwiki.sock
Accept=no
[Install]
WantedBy=sockets.target
Oddmu service:
# cat /srv/campaignwiki/campaignwiki.service
[Unit]
Description=Oddmu for Campaign Wiki
After=network.target
Requires=campaignwiki.socket
[Install]
WantedBy=multi-user.target
[Service]
User=campaignwiki
Type=simple
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
MemoryHigh=120M
MemoryMax=150M
ExecStart=@/usr/local/bin/oddmu campaignwiki
WorkingDirectory=/srv/campaignwiki/www/wiki
Environment="ODDMU_LANGUAGES=en,de,fr,it"
Environment="ODDMU_WEBFINGER=1"
Oddmuse service:
# cat /srv/campaignwiki/campaignwiki-oddmuse.service
[Unit]
Description=Campaign Wiki (Oddmuse)
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/campaignwiki
Restart=always
User=campaignwiki
MemoryMax=250M
MemoryHigh=220M
ExecStart=/usr/bin/perl /srv/campaignwiki/campaignwiki2.pl daemon --mode production -l http://localhost:4004
Oddmuse mount:
# cat /srv/campaignwiki/campaignwiki2.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/wiki' => '/srv/campaignwiki/campaignwiki.pl'};
app->start;
Oddmuse script:
# cat /srv/campaignwiki/campaignwiki.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin CGI => {
support_semicolon_in_query_string => 1,
};
plugin CGI => {
route => '/',
script => '/srv/campaignwiki/wiki.pl',
run => \&OddMuse::DoWikiRequest,
before => sub {
no warnings;
$OddMuse::RunCGI = 0;
$OddMuse::DataDir = '/srv/campaignwiki/data';
require '/srv/campaignwiki/wiki.pl';
},
};
app->start;
The directory structure is what we already know from elsewhere:
- `data` is the Oddmuse wiki data directory
- `www` is the document root for the website
- `www/wiki` is the semi-public Oddmu data
`data` and `www/wiki` are owned by the `campaignwiki` user.
Various directories in `www` are owned by the `planet` user: `rpg`, `osr`, `indie`, `jdr`, `podcast`, `podcast-de`, `podcast-fr`.
Monit for both:
# cat /srv/campaignwiki/monit.pl
check process campaignwiki matching "^campaignwiki$"
start program = "/usr/bin/systemctl start campaignwiki"
stop program = "/usr/bin/systemctl stop campaignwiki"
restart program = "/usr/bin/systemctl restart campaignwiki"
mode passive
if failed host campaignwiki.org port 443 type tcp ssl protocol http
and request "/view/en/index" for 5 cycles then restart
if totalmem > 150 MB for 5 cycles then restart
check process campaignwiki-oddmuse matching campaignwiki2
start program = "/usr/bin/systemctl start campaignwiki-oddmuse"
stop program = "/usr/bin/systemctl stop campaignwiki-oddmuse"
restart program = "/usr/bin/systemctl restart campaignwiki-oddmuse"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/wiki" for 5 cycles then restart
if totalmem > 250 MB for 5 cycles then restart
Names
The service is called via the `ProxyPassMatch` directive of the `campaignwiki.org` Apache config.
Create a directory under `/run` such that any sockets created therein are all writeable by the `www-data` group:
mkdir /run/names
chown names:www-data /run/names
chmod g+s /run/names
The service creates a socket in the run directory created above:
# cat /srv/names/names.service
[Unit]
Description=Names
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/names
Restart=always
User=names
MemoryMax=250M
MemoryHigh=220M
UMask=002
ExecStart=/usr/bin/perl names2.pl daemon --mode production -l "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/names/run
chown names:www-data /srv/names/run
chmod g+s /srv/names/run
The wrapper script:
# cat /srv/names/names2.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/names' => '/srv/names/names.pl'};
app->start;
`names.pl` is the actual Mojolicious application.
Monit:
# cat /srv/names/monit.conf
check process names matching names
start program = "/usr/bin/systemctl start names"
stop program = "/usr/bin/systemctl stop names"
restart program = "/usr/bin/systemctl restart names"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/names" for 5 cycles then restart
if totalmem > 250 MB for 5 cycles then restart
Traveller
The service is called via the `ProxyPassMatch` directive of the `campaignwiki.org` Apache config.
Create a directory under `/run` such that any sockets created therein are all writeable by the `www-data` group:
mkdir /run/traveller
chown traveller:www-data /run/traveller
chmod g+s /run/traveller
The service creates a socket in the run directory created above:
[Unit]
Description=Traveller
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/traveller
Restart=always
User=traveller
MemoryMax=300M
MemoryHigh=290M
UMask=002
ExecStart=/usr/bin/perl -Iperl/share/perl/5.40.1 traveller.pl daemon --mode production -l "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/traveller/run
chown traveller:www-data /srv/traveller/run
chmod g+s /srv/traveller/run
Wraper script:
# cat /srv/traveller/traveller.pl
use Mojolicious::Lite -signatures;
use File::Basename;
plugin Mount => {'/traveller' => "/srv/traveller/perl/bin/traveller"};
app->start;
Install the app itself:
mkdir /srv/traveller/perl
cd /srv/traveller/perl
wget https://cpan.metacpan.org/authors/id/S/SC/SCHROEDER/App-traveller-1.02.tar.gz
tar xzf *.tar.gz
cd App-traveller-1.02
perl Makefile.PL prefix=/srv/traveller/perl
make install
cd ..
rm -rf App-traveller*
Monit:
# cat /srv/traveller/monit.conf
check process traveller matching traveller
start program = "/usr/bin/systemctl start traveller"
stop program = "/usr/bin/systemctl stop traveller"
restart program = "/usr/bin/systemctl restart traveller"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/traveller" for 5 cycles then restart
if totalmem > 300 MB for 5 cycles then restart
Character Sheet Generator
Install the software from a tarball, as shown for Traveller, above.
Create a directory under `/run` such that any sockets created therein are all writeable by the `www-data` group, as shown for Traveller, above.
Wrapper script:
# cat /srv/character-sheet-generator/character-sheet-generator.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/character-sheet-generator' => "/srv/character-sheet-generator/perl/bin/character-sheet-generator"};
app->start;
Service:
# cat character-sheet-generator/character-sheet-generator.service
[Unit]
Description=Character Sheet Generator
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/character-sheet-generator
Restart=always
User=character-sheet-generator
MemoryMax=100M
MemoryHigh=80M
UMask=002
ExecStart=/usr/bin/perl -Iperl/share/perl/5.40.1 character-sheet-generator.pl daemon --mode production -l "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/character-sheet-generator/run
chown names:www-data /srv/character-sheet-generator/run
chmod g+s /srv/character-sheet-generator/run
Config file to tell it about the face generator:
# cat character-sheet-generator/character-sheet-generator.conf
# -*- mode: perl -*-
{
face_generator_url => "https://campaignwiki.org/face",
}
Monit:
# cat character-sheet-generator/monit.conf
check process character-sheet-generator matching character-sheet-generator
start program = "/usr/bin/systemctl start character-sheet-generator"
stop program = "/usr/bin/systemctl stop character-sheet-generator"
restart program = "/usr/bin/systemctl restart character-sheet-generator"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/character-sheet-generator" for 5 cycles then restart
if totalmem > 250 MB for 5 cycles then restart
Text Mapper
This uses the same setup.
Install the software from a tarball, as shown for Traveller, above.
Create a directory under `/run` such that any sockets created therein are all writeable by the `www-data` group, as shown for Traveller, above.
Wrapper script:
# cat /srv/text-mapper/text-mapper.pl
use Mojolicious::Lite -signatures;
plugin Mount => {'/text-mapper' => "/srv/text-mapper/perl/bin/text-mapper"};
app->start;
Service:
# cat /srv/text-mapper/text-mapper.service
[Unit]
Description=Text Mapper
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=text-mapper
WorkingDirectory=/srv/text-mapper
Restart=always
MemoryMax=200M
MemoryHigh=180M
UMask=002
ExecStart=/usr/bin/perl -Iperl/share/perl/5.40.1 text-mapper.pl daemon --mode production -l "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/text-mapper/run
chown text-mapper:www-data /srv/text-mapper/run
chmod g+s /srv/text-mapper/run
Monit:
# cat /srv/text-mapper/monit.conf
check process text-mapper matching text-mapper
start program = "/usr/bin/systemctl start text-mapper"
stop program = "/usr/bin/systemctl stop text-mapper"
restart program = "/usr/bin/systemctl restart text-mapper"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/text-mapper" for 5 cycles then restart
if totalmem > 250 MB for 5 cycles then restart
Face Generator
Install the software from a tarball, as shown for Traveller, above.
Create a directory under `/run` such that any sockets created therein are all writeable by the `www-data` group, as shown for Traveller, above.
Wrapper script:
# cat /srv/face-generator/face-generator.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/face' => "/srv/face-generator/perl/bin/face-generator"};
app->start;
Service:
# cat /srv/face-generator/face-generator.service
[Unit]
Description=Face Generator
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/face-generator
Restart=always
User=face-generator
MemoryMax=600M
MemoryHigh=580M
UMask=002
ExecStart=/usr/bin/perl -Iperl/share/perl/5.40.1 face-generator.pl daemon --mode production -l "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/face-generator/run
chown face-generator:www-data /srv/face-generator/run
chmod g+s /srv/face-generator/run
Config file, with passwords for the various users:
# cat /srv/face-generator/face-generator.conf
# -*- mode: perl -*-
{
secret => '…',
users => {
alex => '…',
alex2 => '…',
alex3 => '…',
airi => '…',
yas => '…',
rorschachhamster => '…',
tuiren => '…'
},
empty => {
tuiren => {
gnome => 'dwarf.png' },
alex => {
dragon => 'dragon.png',
elf => 'elf.png',
dwarf => 'dwarf.png',
gnome => 'dwarf.png',
demon => 'demon.png', }},
no_flip => {
alex => ['dragon', 'demon'],
alex3 => ['dog'] },
}
Monit:
# cat /srv/face-generator/monit.conf
check process face-generator matching face-generator
start program = "/usr/bin/systemctl start face-generator"
stop program = "/usr/bin/systemctl stop face-generator"
restart program = "/usr/bin/systemctl restart face-generator"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/face" for 5 cycles then restart
if totalmem > 600 MB for 5 cycles then restart
Hex Describe
Install the software from a tarball, as shown for Traveller, above.
Create a directory under `/run` such that any sockets created therein are all writeable by the `www-data` group, as shown for Traveller, above.
Wrapper script:
# cat /srv/hex-describe/hex-describe.pl
#! /usr/bin/env perl
use Mojolicious::Lite;
plugin Mount => {'/hex-describe' => "/srv/hex-describe/perl/bin/hex-describe"};
app->start;
Service:
# cat /srv/hex-describe/hex-describe.service
[Unit]
Description=Hex Describe
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/hex-describe
Restart=always
User=hex-describe
MemoryMax=500M
MemoryHigh=400M
UMask=002
Environment="MOJO_INACTIVITY_TIMEOUT=120"
ExecStart=/usr/bin/perl -Iperl/share/perl/5.40.1 hex-describe.pl daemon --mode production -l "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/hex-describe/run
chown hex-describe:www-data /srv/hex-describe/run
chmod g+s /srv/hex-describe/run
Config file to tell it about Text Mapper and Face Generator:
# cat hex-describe/hex-describe.conf
# -*- mode: perl -*-
{
text_mapper_url => 'https://campaignwiki.org/text-mapper',
face_generator_url => 'https//campaignwiki.org/face',
}
Monit:
# cat /srv/hex-describe/monit.conf
check process hex-describe matching hex-describe
start program = "/usr/bin/systemctl start hex-describe"
stop program = "/usr/bin/systemctl stop hex-describe"
restart program = "/usr/bin/systemctl restart hex-describe"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/hex-describe" for 5 cycles then restart
if totalmem > 300 MB for 5 cycles then restart
INN2 (net news)
See my peering notes.
peering
When I moved virtual machines my INN installation was borked. Specifically, when I used `rtin -A` to connect, with `~/.newsauth` set, subscribed to a few newsgroups (using `S` and a few patterns), and tried to enter them, it told me, "no articles". But when I used `ls /var/spool/articles/…` I saw articles!
Interestingly, I could only get the old (!) articles via telnet. The newer ones don't show up.
telnet campaignwiki.org 119
MODE READER
GROUP campaignwiki.talk
ARTICLE 1
-- prints some stuff
ARTICLE 300
-- says no such article number
All the files are `-rw-rw-r-- 1 news news` so I'm thinking: is there some sort of index that I need to regenerate? And of course there is.
This is a Debian stable server, with very little fiddling of the settings. Most importantly, `/etc/news/storage.conf` says that the storage method is `tradspool` for all newsgroups. This installation uses `tradindexes` and `find /var/spool/news/overview/ -name "*.IDX" | xargs ls -l` shows that some of the newsgroups I care about have size 0. Time to read up on `tdx-util`.
Trying to rebuild an index with `/usr/lib/news/bin/tdx-util -R /var/spool/news/articles/grenzland/test -n grenzland.test` (should have used `sudo -u news`) gives me an error for every single article: `tdx-util: cannot find article <…> in history`. Time to read up on `makehistory`.
`sudo -u news /usr/lib/news/bin/makehistory -O` and I'm getting another error: `makehistory: cannot open /run/news/.rebuildoverview: No such file or directory`. And I'm not sure how to take that since I stopped the `inn2.service` before doing this. And with that, I'm not surprised that stuff in `/run` is gone. As `/run/news` is the `pathrun` option in `inn.conf` I'm assuming it's safe to create it while `inn` is not running.
mkdir /run/news
chown news:news /run/news
sudo -u news /usr/lib/news/bin/makehistory -O
find /var/spool/news/overview/ -name "*.DAT" | xargs ls -l
Now it reports non-zero sizes for the things I care about. It seems to work!
News web application
This should be a different user than `news`.
adduser news-app --system
We still keep the files in `/srv/news`.
Service:
# cat news.service
[Unit]
Description=News
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/news
Restart=always
User=news-app
MemoryMax=300M
MemoryHigh=290M
Environment="NNTPSERVER=localhost"
Environment="NEWS_INTRO_ID=<u4d0i0$n72d$1@sibirocobombus.campaignwiki>"
UMask=002
ExecStart=/usr/bin/perl -Iperl/share/perl/5.40.1 perl/bin/news daemon --mode production -l "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/news/run
chown news-app:www-data /srv/news/run
chmod g+s /srv/news/run
Monit:
# cat /srv/news/monit.conf
check process news matching perl/bin/news
start program = "/usr/bin/systemctl start news"
stop program = "/usr/bin/systemctl stop news"
restart program = "/usr/bin/systemctl restart news"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/news" for 5 cycles then restart
if totalmem > 300 MB for 5 cycles then restart
`stunnel4` for INN2
Configure:
# cat /etc/stunnel4/nntps.conf
debug = info
[nntps]
accept = 563
setuid = news
setgid = news
exec = /usr/lib/news/bin/nnrpd
key = /etc/apache2/md/domains/campaignwiki.org/privkey.pem
cert = /etc/apache2/md/domains/campaignwiki.org/pubcert.pem
This uses the web server certificates created by `mod_md`.
`ngircd`
This is the IRC server that is connected to two other servers.
- campaignwiki.org connects to wilderland.ovh on port 6697 using TLS
- grenzland.club connects to wilderland.ovh on port 6697 using TLS
- if wilderland.ovh cannot be reached, campaignwiki.org connects to grenzland.club on port 6697 using TLS (they are in the same group)
# cat /etc/ngircd/ngircd.conf
[Global]
Name = campaignwiki.org
AdminInfo1 = Alex Schroeder
AdminInfo2 = Zuerich
AdminEMail = alex@alexschroeder.ch
Info = IRC for role-playing games
MotdFile = /etc/ngircd/ngircd.motd
PidFile = /run/ngircd/ngircd.pid
Ports = 6667
ServerGID = irc
ServerUID = irc
[Limits]
ConnectRetry = 60
MaxConnections = 500
MaxConnectionsIP = 10
MaxJoins = 40
MaxNickLength = 20
MaxListSize = 100
PingTimeout = 120
PongTimeout = 60
[Options]
CloakHost = internet
CloakHostModeX = intranet
CloakUserToNick = yes
DNS = no
Ident = no
MorePrivacy = yes
OperCanUseMode = yes
PAM = no
SyslogFacility = daemon
[SSL]
CertFile = /etc/ngircd/cert.pem
CipherList = SECURE128:-VERS-SSL3.0
DHFile = /etc/ngircd/dhparams.pem
KeyFile = /etc/ngircd/key.pem
Ports = 6697
[Operator]
Name = kensanata
Password = …
[Server]
Name = wilderland.ovh
Host = wilderland.ovh
Port = 6697
SSLConnect = yes
SSLVerify = no
MyPassword = …
PeerPassword = …
Group = 1
[Server]
Name = grenzland.club
Host = grenzland.club
Port = 6697
SSLConnect = yes
SSLVerify = no
MyPassword = …
PeerPassword = …
Group = 1
[Channel]
Name = #welcome
Topic = Chit chat and hanging out
Planet Jupiter
There are a number of planets this runs: the blogs I follow, RPG Planet, Old School RPG Planet, Indie RPG Planet, RPG Podcast Planet, Planète des JDR, Podcast Planète des JDR, Rollenspiel Podcast Planet.
the blogs I follow
RPG Planet
Old School RPG Planet
Indie RPG Planet
RPG Podcast Planet
Planète des JDR
Podcast Planète des JDR
Rollenspiel Podcast Planet
The OPML files are available from the respective sites.
Download all the blog feeds:
# cat /srv/planet/planet-update
#!/bin/sh
perl -I/srv/planet/perl/share/perl/5.40.1 /srv/planet/perl/bin/jupiter --log=warn update \
indie.opml osr.opml other.opml alex.opml podcast.opml podcast-de.opml podcast-fr.opml jdr.opml
Timer:
# cat /srv/planet/planet-update.timer
[Unit]
Description=Planet Update
[Timer]
OnCalendar=*-*-* 03,07,11,15,19,23:40:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Service:
# cat /srv/planet/planet-update.service
[Unit]
Description=Planet Update
[Service]
Type=oneshot
User=planet
WorkingDirectory=/srv/planet
ExecStart=/srv/planet/planet-update
Assemble them into web sites using the templates I wrote for them (not linked):
# cat /srv/planet/planet-html
#!/bin/sh
perl/bin/jupiter html indie.html indie-template.html indie.xml indie.rss indie.opml \
&& mv indie.html /srv/campaignwiki/www/indie/index.html \
&& cp indie.xml /srv/campaignwiki/www/indie/feed.xml \
&& cp indie.opml /srv/campaignwiki/www/indie/
perl/bin/jupiter html osr.html osr-template.html osr.xml osr.rss osr.opml \
&& mv osr.html /srv/campaignwiki/www/osr/index.html \
&& cp osr.xml /srv/campaignwiki/www/osr/feed.xml \
&& cp osr.opml /srv/campaignwiki/www/osr/
perl/bin/jupiter html rpg.html rpg-template.html rpg.xml rpg.rss indie.opml osr.opml other.opml \
&& mv rpg.html /srv/campaignwiki/www/rpg/index.html \
&& cp rpg.xml /srv/campaignwiki/www/rpg/feed.xml \
&& cp indie.opml osr.opml other.opml /srv/campaignwiki/www/rpg/
perl/bin/jupiter html alex.html alex-template.html alex.xml alex.rss alex.opml \
&& mv alex.html /srv/alexschroeder/www/blogs/index.html \
&& cp alex.xml /srv/alexschroeder/www/blogs/feed.xml \
&& cp alex.opml /srv/alexschroeder/www/blogs/
perl/bin/jupiter html podcast.html podcast-template.html podcast.xml podcast.rss podcast.opml \
&& mv podcast.html /srv/campaignwiki/www/podcast/index.html \
&& cp podcast.xml /srv/campaignwiki/www/podcast/feed.xml \
&& cp podcast.opml /srv/campaignwiki/www/podcast/
perl/bin/jupiter html podcast-de.html podcast-de-template.html podcast-de.xml podcast-de.rss podcast-de.opml \
&& mv podcast-de.html /srv/campaignwiki/www/podcast-de/index.html \
&& cp podcast-de.xml /srv/campaignwiki/www/podcast-de/feed.xml \
&& cp podcast-de.opml /srv/campaignwiki/www/podcast-de/
perl/bin/jupiter html podcast-fr.html podcast-fr-template.html podcast-fr.xml podcast-fr.rss podcast-fr.opml \
&& mv podcast-fr.html /srv/campaignwiki/www/podcast-fr/index.html \
&& cp podcast-fr.xml /srv/campaignwiki/www/podcast-fr/feed.xml \
&& cp podcast-fr.opml /srv/campaignwiki/www/podcast-fr/
perl/bin/jupiter html jdr.html jdr-template.html jdr.xml jdr.rss jdr.opml \
&& mv jdr.html /srv/campaignwiki/www/jdr/index.html \
&& cp jdr.xml /srv/campaignwiki/www/jdr/feed.xml \
&& cp jdr.opml /srv/campaignwiki/www/jdr/
Timer:
# cat /srv/planet/planet-html.timer
[Unit]
Description=Planet Generation
[Timer]
OnCalendar=*-*-* 02,06,10,14,18,22:40:00
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Service:
# cat /srv/planet/planet-html.service
[Unit]
Description=Planet Generation
[Service]
Type=oneshot
User=planet
WorkingDirectory=/srv/planet
ExecStart=/srv/planet/planet-html
Building a dependency that wasn't written by me makes me want to avoid `root`. Here's the installation:
apt install libmodule-build-tiny-perl libfile-sharedir-install-perl
adduser planet --system
mkdir -p /srv/planet/perl
chown -R planet:nogroup /srv/planet/perl
cd /srv/planet/perl
# download
wget https://cpan.metacpan.org/authors/id/D/DO/DOTAN/Mojo-UserAgent-Role-Queued-1.15.tar.gz
wget https://cpan.metacpan.org/authors/id/S/SC/SCHROEDER/App-jupiter-1.09.tar.gz
# unpack
sudo -u planet tar xzf Mojo-UserAgent-Role-Queued-1.15.tar.gz
sudo -u planet tar xzf App-jupiter-1.09.tar.gz
# install
cd Mojo-UserAgent-Role-Queued-1.15
sudo -u planet perl Build.PL
sudo -u planet perl Build install --prefix=/srv/planet/perl
cd ..
rm -rf Mojo-UserAgent-Role-Queued-1.15*
# install
cd App-jupiter-1.09
sudo -u planet perl Makefile.PL PREFIX=/srv/planet/perl
sudo -u planet make install
cd ..
rm -rf App-jupiter-1.09*
Transmission
apt install transmission-daemon
systemctl stop transmission-daemon.service
rsync -ai sibirocobombus:/etc/transmission-daemon/settings.json /etc/transmission-daemon/
rsync -ai sibirocobombus:/var/lib/transmission-daemon/.config /var/lib/transmission-daemon/
In `settings.json`, set the `download-dir` property to `/srv/campaignwiki/www/1pdc/`.
systemctl start transmission-daemon.service
Norn
User:
Installing Norn was a pain. See Build AnyEvent::Discord on Debian for how to build it all. Install the Debian files; then install App-Norn-1.tar.gz by unpacking it in `/srv/norn/perl`:
Build AnyEvent::Discord on Debian
cd /srv/norn/perl
tar xzf App-Norn-1.tar.gz
cd App-Norn-1
perl Makefile.PL prefix=/srv/norn/perl
make install
Service:
# cat /srv/norn/norn.service
[Unit]
Description=Norn
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/norn/
Restart=always
User=norn
MemoryMax=30M
MemoryHigh=20M
ExecStart=/usr/bin/perl -Iperl/share/perl perl/bin/norn
Monit doesn't know if Norn works or not. It might be interesting to write up an IRC connection to see if the user `norn` is online?
# cat monit.conf
check process norn matching norn
start program = "/usr/bin/systemctl start norn"
stop program = "/usr/bin/systemctl stop norn"
restart program = "/usr/bin/systemctl restart norn"
mode passive
if totalmem > 150 MB for 5 cycles then restart
Norn writes its files into `files` so create a symlink:
mkdir /srv/campaignwiki/www/files
chown norn:nogroup /srv/campaignwiki/www/files
ln -s /srv/campaignwiki/www/files/norn /srv/norn/files
Moira
User:
Service:
# cat /srv/moira/moira.service
[Unit]
Description=Moira
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/moira/
Restart=always
User=moira
MemoryMax=80M
MemoryHigh=50M
ExecStart=/usr/bin/perl /srv/moira/moira
Monit doesn't know if Norn works or not.
# cat monit.conf
check process moira matching moira
start program = "/usr/bin/systemctl start moira"
stop program = "/usr/bin/systemctl stop moira"
restart program = "/usr/bin/systemctl restart moira"
mode passive
if totalmem > 150 MB for 5 cycles then restart
Moira knows where to write the XML file because it's part of the Discord server configuration:
# cat DISCORD
token=*secret*
266682102148366336=osr-discord.xml:/srv/campaignwiki/www/files/osr-discord.xml
The local `osr-discord.xml` file is the template and `/srv/campaignwiki/www/files/osr-discord.xml` is the target file.
Roll
The dice roller offers dice rooms. This is a server component.
Service:
# cat /srv/roll/roll.service
[Unit]
Description=Roll
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
WorkingDirectory=/srv/roll
Restart=always
MemoryMax=30M
MemoryHigh=20M
User=roll
UMask=002
ExecStart=perl roll-server.pl daemon \
--mode production \
--inactivity-timeout 3600 \
--keep-alive-timeout 30 \
--listen "http+unix://run%%2Frun.sock"
The `run` directory:
mkdir /srv/roll/run
chown roll:www-data /srv/roll/run
chmod g+s /srv/roll/run
Monit:
# cat /srv/roll/monit.conf
check process roll-server with matching 'roll-server'
start program = "/usr/bin/systemctl start roll"
stop program = "/usr/bin/systemctl stop roll"
restart program = "/usr/bin/systemctl restart roll"
mode passive
if failed host campaignwiki.org port 443 type tcpssl protocol http
and request "/roll-server" for 5 cycles then restart
if totalmem > 100 MB for 5 cycles then restart
TODO
campaignwiki.org:
- document campaignwiki-report
- document campaignwiki-backup
- document galène
- document Prosody incl. share.campaignwiki.org
- do gridmapper-server
- verify `/etc/apache2/hook.sh`
- verify Oddmuse wiki maintenance
Other:
- Exim4
- harden all the services based on `systemd-analyze security` ??
- investigate Mojolicious and the 'under' config option
- investigate Mojolicious::Plugin::CGI switch from localhost:port to Unix domain sockets
- investigate all the remaining cron jobs