The self-hosted server is a single Go binary that idles at ~45 MB of RAM. The Docker image wraps it with a healthcheck, log rotation, and an auto-update flow that survives container recreates.

This guide walks through downloading the Docker zip from the app, bringing it up with docker compose, and what each file in the zip does.

Prerequisites

  • Docker or Docker Desktop installed (Engine 20.10+ recommended)
  • Two ports reachable from your users:
    • TCP 8088: WebSocket (chat, signaling) and HTTP (file downloads)
    • UDP 7070: Voice traffic (WebRTC)
  • A GameVox account and your server’s download page open in the app

User accounts flow through the GameVox cloud for authentication. Everything else (messages, voice, files, emojis, soundboard clips, permissions) lives in your container’s data volume.

1. Download the Docker zip

From your server’s download page in the GameVox app, select the Docker option. You’ll get a gamevox-docker.zip containing four files:

gamevox-docker.zip
  gamevox-docker/
    gamevox-self-hosted-server    (Linux binary with your server config embedded)
    Dockerfile                    (Alpine image, reads TCP_PORT / UDP_PORT / DATA_DIR)
    entrypoint.sh                 (seeds the binary into the data volume so auto-updates persist)
    docker-compose.yml            (host-networked, ports and PUBLIC_IP set near the top)

Your server’s configuration is baked into the binary, so there’s no separate token to paste. Keep the zip private; anyone with it can connect as your server.

2. Bring it up with Docker Compose

Extract the zip, open a terminal in the gamevox-docker/ folder, and run:

docker compose up -d --build

That builds the image and starts the container. The shipped Compose file uses host networking by default. The container shares the host’s network stack directly, which works best for voice/WebRTC: LAN clients reach the server at the host’s LAN IP without extra config, and WebRTC sees the real network interfaces so ICE candidate gathering doesn’t break behind Docker’s bridge NAT. Host networking is Linux-only. On Docker Desktop (macOS/Windows), see the bridge-mode note in §3.

Check that it’s running:

docker compose ps
docker compose logs -f gamevox

You should see the server connecting to the GameVox cloud. From the host, hit the health endpoint to confirm the internal server is up:

curl -k https://localhost:8088/health

The -k is needed because the server uses a self-signed cert for HTTPS on localhost.

3. What’s in the zip

The four files are small and commented. Here’s what each one does.

docker-compose.yml

Two things to know about the shipped Compose file:

  • YAML anchors at the top. Ports are declared once in an x-config block and referenced from the environment and healthcheck blocks via *tcp_port / *udp_port. To change a port, edit one value at the top of the file.
  • Host networking by default. Set with network_mode: host. A commented-out ports: block underneath is the bridge-mode fallback for Docker Desktop, or hosts where you can’t use host mode.
# Edit these two values to change the server's ports. They flow into the
# environment: and healthcheck: blocks below via YAML anchors, so you only
# need to change them in this one place.
x-config:
  tcp_port: &tcp_port 8088        # WebSocket (chat, signaling) + HTTP (file downloads)
  udp_port: &udp_port 7070        # Voice traffic (WebRTC/UDP)

services:
  gamevox:
    build: .
    container_name: gamevox

    # Host networking. The container shares the host's network stack directly,
    # so LAN clients reach the server at the host's LAN IP without extra config,
    # WebRTC sees real network interfaces (no Docker bridge double-NAT), and
    # the server's built-in LAN auto-discovery works correctly.
    # Linux-only. On Docker Desktop, comment this out and uncomment ports:.
    network_mode: host

    # Bridge networking fallback. Only enable if `network_mode: host` is
    # commented out above. Less reliable for voice; you may also need PUBLIC_IP.
    #ports:
    #  - target: *tcp_port
    #    published: *tcp_port
    #    protocol: tcp
    #  - target: *udp_port
    #    published: *udp_port
    #    protocol: udp

    volumes:
      - ./data:/app/data
    environment:
      DATA_DIR: /app/data
      TCP_PORT: *tcp_port
      UDP_PORT: *udp_port
      # Advanced. Leave commented out unless voice connections fail and
      # you've confirmed auto-detection grabs the wrong address (multi-IP
      # VPS hosts, asymmetric ingress/egress NAT, or bridge mode above).
      #PUBLIC_IP: 1.2.3.4
    restart: unless-stopped
    healthcheck:
      # $$TCP_PORT (double dollar) tells Compose "leave this alone"; the
      # shell inside the container expands it from the env: block above.
      test: ["CMD-SHELL", "curl -fsk https://localhost:$$TCP_PORT/health"]
      interval: 30s
      timeout: 5s
      start_period: 10s
      retries: 3
    logging:
      driver: json-file
      options:
        max-size: "100m"
        max-file: "3"

Log rotation (100 MB × 3 files) matters because, without it, a chatty server will eventually fill your host’s disk. restart: unless-stopped is what lets the auto-update flow work: when the server exits after swapping its own binary, Docker brings it back up.

Dockerfile

FROM alpine:latest

# Configurable settings. The server binary reads TCP_PORT / UDP_PORT / DATA_DIR
# directly, so these flow straight through to the running process.
ENV TCP_PORT=8088 \
    UDP_PORT=7070 \
    DATA_DIR=/app/data

# curl is used for the HTTPS healthcheck (BusyBox wget lacks SSL support)
RUN apk add --no-cache curl

RUN mkdir -p /app /app/data

WORKDIR /app

VOLUME /app/data

# The binary baked into the image is a "seed". entrypoint.sh copies it into
# the persistent data volume at $DATA_DIR/bin/ on first boot, then always
# execs from there. This makes auto-updates survive container recreates.
COPY ./gamevox-self-hosted-server /app/gamevox-self-hosted-server.seed
COPY ./entrypoint.sh /app/entrypoint.sh
# Set the executable bit inside the build container, so the host's filesystem
# mode is irrelevant. This avoids the "entrypoint.sh not executable" failure
# that hits Windows hosts and other build contexts where exec bits are
# stripped. Plain RUN chmod works without BuildKit (vs. COPY --chmod, which
# requires it).
RUN chmod +x /app/gamevox-self-hosted-server.seed /app/entrypoint.sh

EXPOSE ${TCP_PORT} ${UDP_PORT}/udp

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -fsk https://localhost:${TCP_PORT}/health || exit 1

ENTRYPOINT ["sh", "/app/entrypoint.sh"]

A few details worth knowing:

  • ENV block at the top. Both port values and the data directory live in the image’s environment, so EXPOSE and HEALTHCHECK reference them directly. Override any of them via Compose’s environment: block (the shipped file does this for you) and they flow through cleanly.
  • RUN chmod +x after the COPYs. Sets the executable bit during the build, inside the build container, independent of whatever filesystem mode the host has. That’s why the zip works on Windows hosts and Portainer setups that strip exec bits from the build context. Using a plain RUN line instead of COPY --chmod=755 keeps the build working on Docker engines without BuildKit.
  • .seed rename. The baked-in binary is renamed to .seed inside the image so anyone shelling into the container can tell it apart from the actively-running binary at ${DATA_DIR}/bin/gamevox-self-hosted-server. The file in the downloaded zip keeps its normal name.

Alpine keeps the image small (~15 MB), and only curl is installed on top for the healthcheck. The server binary is fully statically linked, so no extra runtime libraries are needed.

entrypoint.sh

#!/bin/sh
# Gamevox Self-Hosted Server: container entrypoint.
#
# The binary baked into the image is a "seed". On first boot we copy it into
# the persistent data volume at $DATA_DIR/bin/, then always exec from there.
# This way, when the server auto-updates itself (which replaces the binary
# in place on the persistent volume), the new binary survives container
# recreates. Without this, a `docker compose up -d --build` or host reboot
# would drop the updated binary along with the container's writable overlay
# and fall back to the (older) baked-in binary.
#
# If the admin intentionally rebuilds the image with a newer binary (e.g. to
# force a specific version), the seed will be newer than the cached copy and
# the entrypoint will re-seed, overwriting the cached binary.
set -e

BIN_DIR="${DATA_DIR}/bin"
RUN_BIN="${BIN_DIR}/gamevox-self-hosted-server"
SEED="/app/gamevox-self-hosted-server.seed"

mkdir -p "$BIN_DIR"

if [ ! -f "$RUN_BIN" ] || [ "$SEED" -nt "$RUN_BIN" ]; then
  echo "[entrypoint] Seeding binary from image to $RUN_BIN"
  cp "$SEED" "$RUN_BIN"
  chmod +x "$RUN_BIN"
fi

exec "$RUN_BIN" "$@"

This tiny script is what makes auto-updates work reliably in Docker. More on that below.

How auto-updates work in Docker

GameVox servers auto-update themselves: the running process downloads a new binary, swaps it in, and restarts. On bare metal (systemd), this is a one-step process: replace the binary, restart the service.

In Docker it’s trickier. A running container has a writable overlay filesystem, but that overlay is discarded whenever the container is recreated (for example, docker compose up -d --build, or a host reboot in certain configurations). If the updated binary only lived in the overlay, every rebuild would silently roll you back to whatever was baked into the image.

The entrypoint pattern handles this:

  1. First boot. Entrypoint copies the image’s seed binary to /app/data/bin/gamevox-self-hosted-server on the persistent volume, then execs it.
  2. Auto-update. The server writes the new binary directly to /app/data/bin/gamevox-self-hosted-server (the same path it’s running from). On Linux, renaming over a running binary works cleanly.
  3. Restart. The server exits with a special code, Docker’s restart: unless-stopped kicks in, entrypoint runs again, sees the cached binary exists and isn’t older than the seed, and execs it.
  4. Image rebuild. If you docker compose up -d --build with a newer seed binary in the folder, the seed is newer than the cached copy, so the entrypoint re-seeds. Useful for pinning to a specific version.

The takeaway: you don’t need to rebuild the image to stay current. The server updates itself, and the updated binary lives on your data volume.

Manual updates

Auto-updates handle this for you, but if you ever want to force-refresh to the latest version:

# Download a fresh zip from your server's download page, then:
docker compose down
# Replace gamevox-self-hosted-server in your folder with the one from the new zip
docker compose up -d --build

Your ./data/ folder is preserved across this. The entrypoint will see the new seed is newer than the cached binary and re-seed from it.

Directory layout

Before you start, your folder should look like this:

gamevox-docker/
  docker-compose.yml
  Dockerfile
  entrypoint.sh                 (executable bit set by RUN chmod inside the image; nothing to do on the host)
  gamevox-self-hosted-server    (Linux binary from your download page)
  data/                         (created automatically on first run)

After the first run, the data/ folder will fill in with everything the server persists:

data/
  bin/
    gamevox-self-hosted-server  (the actively running binary, seeded from the image)
  community.db                  (chat, channels, permissions, emojis)
  files/                        (user uploads)
  backups/                      (automatic database snapshots)
  logs/                         (server-internal logs, separate from Docker logs)

Useful commands

# View live server logs
docker compose logs -f gamevox

# Restart (e.g. after changing environment variables)
docker compose restart

# Stop the container but keep it around
docker compose stop

# Fully tear down (preserves ./data/)
docker compose down

# Check health status and uptime
docker compose ps

Behind a reverse proxy

If you want to put GameVox behind nginx, Caddy, or Traefik so you can use your own cert on port 443, proxy all traffic on / to http://127.0.0.1:8088. The UDP voice port (7070) cannot go through an HTTP reverse proxy. Expose it directly on the firewall.

nginx example:

server {
    listen 443 ssl http2;
    server_name gamevox.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/gamevox.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/gamevox.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8088;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 3600s;
    }
}

Make sure UDP 7070 is still open on your firewall, since voice traffic bypasses the reverse proxy entirely.

Troubleshooting

The container keeps restarting

Check docker compose logs gamevox. The most common causes:

  • Port already in use. With the default host networking, the server binds 8088/7070 directly on the host. If something else owns those ports, edit the x-config block at the top of docker-compose.yml to pick free ones; the values flow through to both the environment and healthcheck via the YAML anchors.
  • Permission denied on /app/data. The host’s ./data/ directory exists but isn’t writable by the container. If it’s empty, remove it and let Docker recreate it on first run.
  • entrypoint.sh: not executable. Older shipped images used COPY --chmod=755, which requires BuildKit. The current image uses a plain RUN chmod +x line that works on any Docker engine. If you’re seeing this error, redownload the zip to pick up the new Dockerfile.
  • Corrupted seed. Rare, but if the entrypoint copy fails, delete ./data/bin/ and restart. The entrypoint will re-seed from the image.

Voice connections fail or freeze on Docker Desktop

Docker Desktop on macOS/Windows runs containers inside a Linux VM, so network_mode: host maps to the VM’s network rather than the real host LAN. Switch the Compose file to bridge mode:

  1. Comment out network_mode: host.
  2. Uncomment the ports: block underneath it.
  3. If voice still fails, set PUBLIC_IP in the environment: block to the address clients should reach you at.

Auto-updates aren’t happening

The server checks for updates periodically once connected to the GameVox cloud. If you’ve been running an older container from before the entrypoint pattern was added, download a fresh zip once to pick up the new flow. After that, updates are hands-off, with no need to rebuild the image manually.

”I rebuilt the image and lost my data”

You didn’t. Your data lives in ./data/ on the host, which is bind-mounted into the container. As long as that folder is still there, your messages, files, and config are intact. docker compose up -d will pick back up where it left off.

Going further

For a broader overview of what self-hosting includes and how it compares to running on the GameVox cloud, see Self-Hosting: Your Server, Your Hardware. The self-hosted landing page has the full reference for environment variables, firewall rules, and non-Docker install methods (Linux systemd, Windows service).