No accounts·No cookies·No telemetry·Open source·Self-hostable

Home/Run your own Commander table

Run your own Commander table

EDHMat is one small open-source server: Node, SQLite, a single Docker container, no external services beyond Scryfall. What it costs to run and how to put it up.

Updated 2026-08-05

Every other browser tabletop for Commander is a hosted service: you get an account on someone's server, your games live there, and if the project ends, so do the games. EDHMat works that way too if you use the public instance. This page is about the other option, which is the one my own pod uses — the server in my house.

What you are actually running

One container.

  • Node + Fastify, serving both the API and the built web app on a single port.
  • SQLite, one file. Not "SQLite plus Redis", not "SQLite for now". The event log is the database, and backing it up means copying a file.
  • No accounts system, because there are no accounts. Nothing to store, nothing to leak, nothing to reset.
  • One outbound dependency: Scryfall. Card data syncs weekly from their bulk file; card images are hotlinked from their CDN by each player's browser, never proxied or re-hosted.

There is no message broker, no cache tier, no object storage. A four-player game is a few hundred rows of events and a WebSocket per player.

Putting it up

git clone https://github.com/NiBa97/magic-commander-play
cd magic-commander-play
cp .env.example .env    # set SCRYFALL_USER_AGENT to something identifying you
docker compose up -d --build

That is the install. The app serves everything on port 8080 and keeps its database in ./data.

Two settings in .env are worth understanding rather than copying:

  • SCRYFALL_USER_AGENT — Scryfall's API terms require you to identify yourself. Put a real contact address in it; it is how they reach you if your instance misbehaves.
  • PUBLIC_URL — the origin your instance answers on. Set it and every canonical URL, share link and sitemap entry is correct. Leave it unset and the server falls back to the request's own host, which is right on a LAN and wrong behind some proxies.

Behind a reverse proxy

Any TLS terminator works. Caddy, as a concrete example:

your.domain.example {
    reverse_proxy localhost:8080
}

WebSockets pass through without extra configuration — the game connection is a normal HTTP upgrade on the same port as everything else.

First boot takes a few minutes

On first start the server downloads Scryfall's bulk card file in the background (roughly 77 MB) and keeps one printing per card, about 36,000 rows. Until that finishes, deck imports still work; they fall back to Scryfall's rate-limited API and take longer. Nothing is broken, it is warming up.

Which printing gets kept is something I had to make a decision about, because Scryfall's own default printing of a card is its newest one. Left alone, that means a crossover set reprinting a staple takes over your table — in one of our playtest games Command Tower showed up in its Marvel Super Heroes printing, flavour text and all. So the sync scores every printing and keeps the plainest one, and your table looks like Magic.

Running it day to day

Backups. Copy data/app.db (and, if the server is running, the -wal file next to it). That is the entire state: every game, every event, every card the instance has looked up.

Updates. git pull && docker compose up -d --build. Schema migrations are embedded in the code and run on boot.

Disk. The card database is the bulk of it, a couple of hundred megabytes. Games are tiny: the event log of a four-hour game is measured in kilobytes.

Housekeeping. Games untouched for 30 days are garbage-collected automatically. You do not need a cron job.

Health. GET /healthz for your monitoring.

What self-hosting gives you, precisely

You get: game state on hardware you control. No accounts, so nothing to profile. No voice chat, so no audio anywhere. A LAN-only instance that never faces the internet at all, if that is what you want — that is how my pod plays.

You do not get: invisibility. Each player's browser loads card images from Scryfall's CDN, so Scryfall sees image requests. Your voice call is still on whatever platform you hold it. Anyone you invite to your table can screenshot it. Self-hosting removes one intermediary; it is not a privacy system, and I would rather say that here than have you find it out later.

That distinction matters more than usual right now, because the reason a lot of pods started asking about self-hosting was a first-party tool that began processing player voice. Removing a company from the middle of your pod night is a real thing you can do. Becoming untraceable is not.

Is it worth it?

For most pods, no — which is a strange thing to write on my own self-hosting page. The hosted table needs no account either, and it is one click. Self-host if you have a reason: a homelab you already run, a playgroup that cares who holds the data, a LAN with no internet, or plain preference for owning the things you use.

If any of those describe you, it is one container and a file.

Frequently asked questions

Can you self-host a Magic: The Gathering virtual tabletop?

EDHMat can be self-hosted: it is open source and ships as a single Docker container with a SQLite database. Most browser tabletops for Commander are hosted services with no self-host option.

What does EDHMat need to run?

A machine that can run Docker, a couple of hundred megabytes of disk for the card database, and outbound HTTPS to Scryfall for card data. No external database, no cache server, no object storage, no accounts system.

Does self-hosting mean my games are private?

It means the game state lives on your hardware and there are no accounts to profile. Card images are still loaded from Scryfall’s CDN by each player’s browser, and your voice call is still wherever you hold it. Self-hosting removes one intermediary — it does not make you invisible.

Do I need a domain and TLS?

On a LAN, no — a hostname or IP on the local network is enough. On the public internet, put any TLS-terminating reverse proxy in front; WebSockets pass through without special configuration.