Self-hosting
Deploy on Linux
The same deployment on a Linux server: systemd instead of a console window, a non-root user, Postgres from the package manager, and HTTPS in about five minutes.
Read the three addresses first: this page covers only
what is different. Examples use Debian or Ubuntu; on Fedora or Alma, swap apt for
dnf and ufw for firewall-cmd.
What runs where#
┌─────────────────── your Linux server ───────────────────┐
players ───►│ Caddy :443 ──► Flicked.Api :5165 │
│ │ │ │
│ └──────────► Dashboard :3000 │
│ │ │
│ Postgres :5432 (localhost only) │
└─────────────────────────────────────────────────────────┘
│
CS2 + MatchZy ◄──── RCON ──────┘ (same box or another one)Only Caddy faces the internet. The API, the dashboard and Postgres listen on localhost, so a firewall mistake cannot expose them.
1. A user for it#
Never run this as root. It has no reason to write outside its own directory.
sudo useradd --system --home /opt/flicked --shell /usr/sbin/nologin flicked
sudo mkdir -p /opt/flicked/{api,dashboard,keys}
sudo chown -R flicked:flicked /opt/flicked2. Postgres#
sudo apt update
sudo apt install -y postgresql
sudo -u postgres psqlCREATE USER flicked WITH PASSWORD 'pick-a-real-password';
CREATE DATABASE flicked OWNER flicked;
\qCreate no tables: the API migrates itself on startup. Postgres on Debian and Ubuntu listens on localhost
only by default, which is what you want. Docker works properly here, so docker compose up -d with
the repository's docker-compose.yml is a fine alternative.
3. Build and upload#
The API and dashboard are built on your development machine, whatever it runs, and copied across.
# API, targeting Linux
dotnet publish backend/Flicked.Api -c Release -r linux-x64 --self-contained
# dashboard
cd dashboard
npm install
NEXT_PUBLIC_API_URL=https://flicked.example.com npx next buildrsync -a backend/Flicked.Api/bin/Release/net10.0/linux-x64/publish/ you@server:/tmp/api/
rsync -a dashboard/.next/standalone/ you@server:/tmp/dashboard/
rsync -a dashboard/.next/static/ you@server:/tmp/dashboard/.next/static/
rsync -a dashboard/public/ you@server:/tmp/dashboard/public/sudo cp -r /tmp/api/. /opt/flicked/api/
sudo cp -r /tmp/dashboard/. /opt/flicked/dashboard/
sudo chown -R flicked:flicked /opt/flicked
sudo chmod +x /opt/flicked/api/Flicked.ApiThe launcher is still built on Windows#
npm run tauri build produces an installer for the machine it runs on. Your players are on
Windows, so the launcher is built there (see the Windows
guide), with FLICKED_API pointing at this server:
set FLICKED_API=https://flicked.example.comWhat Linux needs that Windows did not#
- ICU. .NET needs
libicufor anything culture-aware. Minimal images lack it, and the failure is an exception on startup:sudo apt install -y libicu-dev. - Node, for the dashboard: install the LTS from NodeSource or your distribution.
4. Configure#
ConnectionStrings:Flicked=Host=localhost;Port=5432;Database=flicked;Username=flicked;Password=<db-password>
# Behind a proxy these are the public https names, not the local port.
Steam:PublicUrl=https://flicked.example.com
Api:PublicUrl=https://flicked.example.com
Dashboard:Url=https://dash.flicked.example.com
# Localhost only: Caddy is the one thing listening publicly.
urls=http://127.0.0.1:5165
STEAM_API_KEY=<your-steam-web-api-key>
FLICKED_ADMIN_STEAM_IDS=<your-steamid64>
DataProtection:KeyPath=/opt/flicked/keysThis file holds your database password and Steam key, so do not leave it world-readable:
sudo chown flicked:flicked /opt/flicked/api/.env
sudo chmod 600 /opt/flicked/api/.env
sudo chmod 700 /opt/flicked/keys/opt/flicked/keys holds the keys that encrypt stored RCON passwords. Back it up,
and know that it does not move between machines.
5. Run both as services#
[Unit]
Description=FLICKED API
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
User=flicked
Group=flicked
WorkingDirectory=/opt/flicked/api
ExecStart=/opt/flicked/api/Flicked.Api
Restart=always
RestartSec=5
# It needs to read its own directory and write nothing else.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/flicked/keys
[Install]
WantedBy=multi-user.target[Unit]
Description=FLICKED dashboard
After=network-online.target flicked-api.service
[Service]
User=flicked
Group=flicked
WorkingDirectory=/opt/flicked/dashboard
Environment=PORT=3000
Environment=HOSTNAME=127.0.0.1
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now flicked-api flicked-dashboard
sudo systemctl status flicked-api --no-pager
journalctl -u flicked-api -f # watch the first start apply migrationsProtectSystem=strict makes the whole filesystem read-only except what you name, which is why
ReadWritePaths=/opt/flicked/keys is there. If you move DataProtection:KeyPath, move that
line too, or the API starts and then fails the first time it encrypts anything.
6. HTTPS#
Option A: plain HTTP (testing only)#
Set urls=http://0.0.0.0:5165, open the ports, and stop here. You are then running the setup
whose costs are listed in Security. Fine among friends for an evening, not
otherwise.
sudo ufw allow 5165/tcp
sudo ufw allow 3000/tcpOption B: Caddy (recommended)#
Point two DNS records at the server, then:
sudo apt install -y caddyflicked.example.com {
reverse_proxy 127.0.0.1:5165
}
dash.flicked.example.com {
reverse_proxy 127.0.0.1:3000
}sudo systemctl reload caddy
sudo ufw allow 80/tcp && sudo ufw allow 443/tcpCaddy obtains and renews certificates by itself. Two things to know:
- The session cookie is marked
Secureonly when the request looks secure, and behind a proxy it arrives over http on loopback. The API therefore readsX-Forwarded-Proto, but only from a proxy on the same machine, because those headers are trivial to forge. If your proxy is on a different machine, add its address toKnownProxiesinProgram.cs, or the cookie silently loses itsSecureflag. - Rebuild the launcher once the addresses are
https://. The address is compiled in.
7. The CS2 server#
MatchZy, Metamod and CounterStrikeSharp all run on Linux, and the backend does not care which the server is:
sudo -u steam /opt/steamcmd/steamcmd.sh +force_install_dir /opt/cs2 \
+login anonymous +app_update 730 validate +quitgameinfo.giis restored byvalidate, exactly as on Windows. Re-add the Metamod search path after every update. See Running the CS2 server.- The RCON port must be reachable from the API. Same box means
127.0.0.1:27015and no firewall rule; another box means opening 27015 to this server's address only, not to the world.
8. Keep the clock right#
Match timers are drawn from timestamps, so a drifting clock shows a countdown that starts part-way through or sits at zero:
timedatectl status # want: "System clock synchronized: yes"
sudo timedatectl set-ntp trueUpgrading#
# on your machine
dotnet publish backend/Flicked.Api -c Release -r linux-x64 --self-contained
rsync -a backend/Flicked.Api/bin/Release/net10.0/linux-x64/publish/ you@server:/tmp/api/
# on the server
sudo -u postgres pg_dump flicked > ~/flicked-$(date +%F).sql # before a data migration
sudo systemctl stop flicked-api
sudo cp -r /tmp/api/. /opt/flicked/api/
sudo chown -R flicked:flicked /opt/flicked/api
sudo chmod +x /opt/flicked/api/Flicked.Api
sudo systemctl start flicked-api
journalctl -u flicked-api -n 50 --no-pagerUnless the dependencies changed, only Flicked.Api.dll and Flicked.Api.pdb differ, so
copying those two is enough.
Troubleshooting#
Everything in the Windows troubleshooting table still applies. These are the Linux-only ones.
| Symptom | Cause |
|---|---|
Permission denied starting the service | The published binary has no execute bit: chmod +x |
| Service starts, then fails encrypting something | ProtectSystem=strict without ReadWritePaths for the key folder |
| Culture or globalization exception on startup | libicu missing, typical of minimal images |
Reachable on :5165 but not through Caddy | Caddy is proxying a different port than the API listens on |
| Dashboard cookie rejected behind the proxy | Dashboard and API are on hostnames that are not the same site |
FATAL: password authentication failed | The password in .env does not match the flicked role |
| Everything works until reboot | The services were never systemctl enabled |