Running OSCAR yourself¶
The faculty server pulls the image from an internal registry and updates on a timer. That is out of your hands. This page shows how to run the bot on your own machine or home server instead.
The bot only opens outgoing connections, to discord.com and to
cloud.ovgu.de. Both are public. You need no university network, no VPN and no
open ports.
One bot, one instance
A Discord token may only run in one place at a time. If the faculty container and yours use the same token, both answer every command. Use your own test application while the faculty one is running.
1. Create your own bot¶
- Open the Discord Developer Portal and click New Application.
- Go to Bot and press Reset Token. Copy the token.
- On the same page enable Message Content Intent. The prefix commands such as
!resyncneed it. - Go to OAuth2 → URL Generator. Tick the scopes
botandapplications.commands. Without the second one no slash command can register. - Under Bot Permissions tick Send Messages, Embed Links and Use Slash Commands.
- Open the generated link and invite the bot to a test server.
2. Fill in the settings¶
Then edit .env:
| Variable | Where it comes from |
|---|---|
BOT_TOKEN |
the token from step 1 |
DISCORD_SERVER_ID |
Developer Mode on, right click the server, Copy Server ID |
TABLES_URL |
already filled in, https://cloud.ovgu.de/apps/tables/ |
TABLES_USERNAME |
your Nextcloud account |
TABLES_PASSWORD |
cloud.ovgu.de → Settings → Security → App passwords |
.env is in .gitignore. Never commit it.
3. Start it¶
The log should show Logged in as ... followed by the number of synced commands.
compose.yaml uses Containerfile.selfhost. Unlike the CI Containerfile it builds
the wheel itself, so no artifact from a pipeline is needed, and it bakes in no
secrets. They arrive at runtime from .env.
Both files sit on python:3.14-slim-trixie. The finished image is about 520 MB on
disk. The compiler toolchain of the full image is only needed while the wheel is
built, and that happens in a first stage that is thrown away.
4. Check which build is running¶
Type !resync on your home server, the one DISCORD_SERVER_ID names, or /version
anywhere. !resync answers to the owner of the application and to the administrators
of the home server. Both commands name the build:
!resync is a prefix command. It works even when the slash menu is still empty,
which makes it the reliable way to see what is actually deployed.
The build only carries a useful name if you stamp it. Without the variable it stays
the word selfhost, which tells you nothing:
On Windows PowerShell:
If the slash menu stays empty in the client, press Ctrl+R in Discord. The
client caches the command list.
Making commands appear at once¶
By default the commands are registered globally. That reaches every server, but a client can take up to an hour to notice a change.
While developing, add this to .env:
Now they are registered only in the servers the bot is in, and they appear immediately.
Never both scopes
Discord keeps global and guild commands apart and lists both. A command registered in both scopes shows up twice in the picker. Whichever scope is set, OSCAR empties the other one, so this cannot happen by accident. Switching the value once and restarting is enough to clean up.
Everyday commands¶
docker compose logs -f # follow the log
docker compose restart # restart
docker compose up -d --build # rebuild after a code change
docker compose down # stop, the database volume stays
Keeping the database¶
The SQLite file lives in a named volume, mounted at /database. A rebuild keeps it.
docker compose down -v deletes it, so avoid that flag.
compose.yaml calls the volume oscar-db, but Docker prefixes it with the project
name, which is the directory you start from. In a checkout called discord-bot the
real name is discord-bot_oscar-db. Ask Docker rather than guessing:
Copy the database file once, by hand:
That is fine while the bot is stopped. While it runs, a plain copy can catch the file in the middle of a write. Backing it up every night uses the SQLite backup API instead.
Restoring overwrites a file SQLite may have open, so stop the bot first:
docker compose stop
docker compose cp ./oscar-backup.db oscar:/database/oscar.db
docker compose start
Running it on a home server¶
Nothing about the bot needs a data centre. It opens no listening port and needs no university network, so a machine at home behind a normal router is enough. Nothing has to be forwarded, and the server does not need to be reachable from outside.
Surviving a reboot¶
compose.yaml already sets restart: unless-stopped, so Docker starts the container
again after a crash and after a reboot. That only holds if the Docker service itself
starts at boot:
Check it once by rebooting and then asking how long the container has been up:
unless-stopped remembers what you did
If you stop the container with docker compose stop, Docker leaves it stopped
across a reboot. That is the point of the policy. Start it again with
docker compose start.
Updating to a newer version¶
Pull the code, rebuild with the commit as the build stamp, restart:
The stamp matters. Without it every build calls itself selfhost and !resync can
no longer tell you which version is answering.
The database volume survives this. Only the image is replaced.
Backing it up every night¶
The database holds what students saved, and it lives in one volume. tools/oscar-backup.sh
copies it with the SQLite backup API, checks that the copy opens, and deletes copies
older than 14 days. A copy is personal data like the original: the script keeps it in
/var/backups/oscar, readable by root only. The 14 days are what the
privacy page promises, change both together.
sudo install -m 700 tools/oscar-backup.sh /usr/local/sbin/oscar-backup
sudo /usr/local/sbin/oscar-backup # run it once and read what it says
Run it every night with a systemd timer:
[Unit]
Description=Copy the OSCAR database to /var/backups/oscar
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/oscar-backup
[Unit]
Description=Nightly copy of the OSCAR database
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
The copies sit on the same disk as the original. They save you from a bad update and from a deleted volume, not from a dead disk. Copy the folder to a second machine if that matters to you, and keep it as closed there as it is here.
Running it without root¶
The container needs to write to /database and to a scratch /tmp, nothing else. Give
the volume to an unprivileged user once, while the bot is stopped:
docker compose down
docker run --rm --user 0 -v discord-bot_oscar-db:/database --entrypoint chown oscar-ovgu:selfhost -R 10001:10001 /database
Start the bot once before you do this
The step only holds for a volume that already has the database in it. Docker copies
the owner of /database from the image into an empty volume every time a
container mounts it, which would undo the change. So run the bot once the normal
way, let it create oscar.db, and harden it afterwards.
Then add this to the oscar service in compose.yaml and start it again:
environment:
LOGURU_LEVEL: INFO # the debug lines are not worth keeping
HOME: /tmp
user: "10001:10001"
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
A bug in the bot or in a library can then do no more than the bot itself. Check it:
Keeping the log small¶
compose.yaml caps the container log at five files of 10 MB, so it can never fill
the disk. That is enough for several days of history. Read it with
docker compose logs -f, or a slice of it with docker compose logs --since 1h.
Reaching the server¶
You never need to reach the bot. It talks to Discord, Discord does not talk to it.
You do need to reach the server to update it. SSH on the local network is enough at home. Tailscale is one way to get the same SSH access from anywhere without opening a port on the router, but it is a convenience for you, not a requirement for OSCAR.