Skip to content
FLICKED Docs

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#

one Linux server
            ┌─────────────────── 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.

server
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/flicked

2. Postgres#

server
sudo apt update
sudo apt install -y postgresql
sudo -u postgres psql
psql
CREATE USER flicked WITH PASSWORD 'pick-a-real-password';
CREATE DATABASE flicked OWNER flicked;
\q

Create 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.

your machine
# 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 build
your machine
rsync -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/
server
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.Api

The 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:

Windows build machine
set FLICKED_API=https://flicked.example.com

What Linux needs that Windows did not#

  • ICU. .NET needs libicu for 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#

/opt/flicked/api/.env
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/keys

This file holds your database password and Steam key, so do not leave it world-readable:

server
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#

/etc/systemd/system/flicked-api.service
[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
/etc/systemd/system/flicked-dashboard.service
[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.target
server
sudo 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 migrations

ProtectSystem=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.

server
sudo ufw allow 5165/tcp
sudo ufw allow 3000/tcp

Point two DNS records at the server, then:

server
sudo apt install -y caddy
/etc/caddy/Caddyfile
flicked.example.com {
    reverse_proxy 127.0.0.1:5165
}

dash.flicked.example.com {
    reverse_proxy 127.0.0.1:3000
}
server
sudo systemctl reload caddy
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp

Caddy obtains and renews certificates by itself. Two things to know:

  • The session cookie is marked Secure only when the request looks secure, and behind a proxy it arrives over http on loopback. The API therefore reads X-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 to KnownProxies in Program.cs, or the cookie silently loses its Secure flag.
  • 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:

server
sudo -u steam /opt/steamcmd/steamcmd.sh +force_install_dir /opt/cs2 \
     +login anonymous +app_update 730 validate +quit
  • gameinfo.gi is restored by validate, 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:27015 and 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:

server
timedatectl status          # want: "System clock synchronized: yes"
sudo timedatectl set-ntp true

Upgrading#

upgrade
# 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-pager

Unless 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.

SymptomCause
Permission denied starting the serviceThe published binary has no execute bit: chmod +x
Service starts, then fails encrypting somethingProtectSystem=strict without ReadWritePaths for the key folder
Culture or globalization exception on startuplibicu missing, typical of minimal images
Reachable on :5165 but not through CaddyCaddy is proxying a different port than the API listens on
Dashboard cookie rejected behind the proxyDashboard and API are on hostnames that are not the same site
FATAL: password authentication failedThe password in .env does not match the flicked role
Everything works until rebootThe services were never systemctl enabled
Esc
↑↓ navigate ↵ open