Run Your Own Campfire on .NET with Docker

This post is part of the Campfire series. The earlier posts were about what made the C# port fast. This one and the next are about running it: here in Docker, and next time inside a LINQPad script.

The C# port was built as a drop-in for Basecamp’s Rails image. It keeps its data in the same place, reads the same settings and listens on the same ports. If you know how to run Basecamp’s Campfire, you already know how to run this one. Change the image name and you’re done.

The image is published to GitHub Container Registry for both Intel/AMD and ARM machines:

ghcr.io/ryanrodemoyer/once-campfire-csharp:latest

Like Basecamp’s image, there’s a tag for every release (for example :1.0.0), and :1.0 follows the latest patch of a minor version.

Try it on your laptop

You need Docker and one secret. This starts Campfire on http://localhost:8080 without HTTPS:

docker run --name campfire \
  --publish 8080:80 \
  --volume campfire:/rails/storage \
  --env SECRET_KEY_BASE=$(openssl rand -hex 64) \
  --env DISABLE_SSL=true \
  ghcr.io/ryanrodemoyer/once-campfire-csharp:latest

Open the page and you’ll be walked through creating the first admin account. The email you enter for that account is shown on the sign-in page, so people who forget their password know who to ask.

The campfire volume holds everything: the SQLite database, uploaded files and backups. Delete the container and recreate it, and your rooms and messages are still there.

Generate your secrets

For anything you’ll keep, generate the secrets once and store them somewhere safe:

  • SECRET_KEY_BASE signs and encrypts cookies. Change it and everyone is signed out.
  • VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY are the key pair for Web Push notifications. Change them and everyone has to turn notifications on again.

The image generates them for you, with the same script Basecamp’s image has:

docker run --rm ghcr.io/ryanrodemoyer/once-campfire-csharp:latest script/admin/generate-secrets

It prints three lines you can paste into an environment file:

SECRET_KEY_BASE=...
VAPID_PRIVATE_KEY=...
VAPID_PUBLIC_KEY=...

Run it on a server with HTTPS

Point a domain at your server and open ports 80 and 443. Set TLS_DOMAIN, and the container gets and renews a Let’s Encrypt certificate itself:

docker run --detach --name campfire \
  --restart unless-stopped \
  --publish 80:80 --publish 443:443 \
  --volume campfire:/rails/storage \
  --env SECRET_KEY_BASE=$YOUR_SECRET_KEY_BASE \
  --env VAPID_PUBLIC_KEY=$YOUR_PUBLIC_KEY \
  --env VAPID_PRIVATE_KEY=$YOUR_PRIVATE_KEY \
  --env TLS_DOMAIN=chat.example.com \
  ghcr.io/ryanrodemoyer/once-campfire-csharp:latest

Or the same thing with Docker Compose:

services:
  web:
    image: ghcr.io/ryanrodemoyer/once-campfire-csharp:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    env_file: campfire.env   # SECRET_KEY_BASE, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY
    environment:
      - TLS_DOMAIN=chat.example.com
    volumes:
      - campfire:/rails/storage

volumes:
  campfire:

Keep campfire.env out of source control.

Already behind a proxy?

The image includes Thruster, the same small proxy Basecamp’s image uses for HTTPS, compression and certificates. If you already run nginx, Caddy, Traefik or a cloud load balancer in front of your apps, you can turn Thruster off and have Kestrel answer directly:

  --env CAMPFIRE_THRUSTER=0 \
  --env DISABLE_SSL=true \

The app then listens on port 80 inside the container, and your proxy handles HTTPS. This switch came from a pull request by Jose Luis Chavez del Cid (@jlchavez), and it’s how the C# port runs in the benchmark.

Switch an existing Rails Campfire to C#

This is the part that makes it a drop-in. The C# app uses the same SQLite schema, the same file layout and the same cookie format, so your users stay signed in.

  1. Back up first (see below), using the Rails container.
  2. Stop the Rails container, but keep its volume.
  3. Start the C# image with the same volume and the same SECRET_KEY_BASE and VAPID keys. Reusing the secrets is what keeps sessions and push notifications working.
docker stop campfire && docker rm campfire

docker run --detach --name campfire \
  --restart unless-stopped \
  --publish 80:80 --publish 443:443 \
  --volume campfire:/rails/storage \
  --env-file campfire.env \
  --env TLS_DOMAIN=chat.example.com \
  ghcr.io/ryanrodemoyer/once-campfire-csharp:latest

Going back works the same way: stop the C# container and start Basecamp’s image on the same volume. The port’s tests include Rails reading, editing and searching messages the C# app wrote.

Two things to know:

  • Switch while the app is stopped. Don’t run both images against the same volume at once.
  • Read the known differences first. Today there’s one, about when the database’s write-ahead log gets checkpointed, and it doesn’t change what users see.

Back up and restore

All the state lives in the volume. To take a consistent copy of the database while Campfire is running, ask it for a snapshot first, with the same script Basecamp’s image uses:

docker exec campfire script/admin/prepare-backup

The snapshot goes to storage/backups/ inside the volume. Then archive the whole volume to the current directory:

docker run --rm --user root \
  --volume campfire:/rails/storage \
  --volume "$PWD":/backup \
  ghcr.io/ryanrodemoyer/once-campfire-csharp:latest \
  tar czf /backup/campfire-backup.tar.gz -C /rails storage

Copy campfire-backup.tar.gz somewhere off the machine. To restore, stop Campfire, extract the archive into the volume, and copy the snapshot over the live database:

docker run --rm --user root \
  --volume campfire:/rails/storage \
  --volume "$PWD":/backup \
  ghcr.io/ryanrodemoyer/once-campfire-csharp:latest \
  bash -c "tar xzf /backup/campfire-backup.tar.gz -C /rails &&
           cp /rails/storage/backups/production.sqlite3 /rails/storage/db/production.sqlite3 &&
           rm -f /rails/storage/db/production.sqlite3-wal /rails/storage/db/production.sqlite3-shm &&
           chown -R rails:rails /rails/storage"

Upgrade

Pull the newer image and recreate the container with the same volume and settings:

docker pull ghcr.io/ryanrodemoyer/once-campfire-csharp:latest

Any pending database migrations run when the container starts.

If something’s wrong

The C# port aims to behave exactly like Rails. If you find a difference, that’s a bug, and I’d like to hear about it: open an issue with what you did, what Rails does and what the C# port did instead.

csharp dotnet docker self-hosting campfire sqlite kestrel ghcr