Campfire on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Campfire on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Campfire is a web based chat application for teams. It gives a team the things it actually needs to talk all day: multiple rooms with access controls so a conversation can be open to everyone or closed to a few, one to one direct messages, file attachments with inline previews, @mentions that notify the right person, and full text search across the whole history so nothing said last month is lost. Web Push notifications reach people who have the tab closed, and a simple bot API lets scripts and services post into a room.
The cloudimg image ships the free and open source, MIT licensed Campfire from the official upstream container image, pinned by both tag and digest so the deployment can never silently float to a different build. Campfire is deliberately a single machine product: one instance holds the whole team's conversation, and all of it, including every uploaded file, stays inside your own Azure subscription. Nothing here ships with a known credential and, just as importantly, nothing ships claimable: a fresh Campfire database leaves the first run wizard open to whoever reaches the machine first, so on first boot this image creates the single administrator itself with a random per instance password, which is what closes that wizard before the instance is reachable. Every VM also generates its own application secret and Web Push key pair. Backed by 24/7 cloudimg support.
Campfire, ONCE and 37signals are trademarks of 37signals, LLC. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by 37signals, LLC. It ships the free and open source MIT licensed self hosted software, unmodified.

What is included:
- Campfire 1.4.5 (MIT licensed, from 37signals) deployed from the official upstream container image, pinned by tag and digest
- A complete single machine stack inside that image: Thruster in front of Puma running Rails 8 on Ruby 3.4.5, a bundled Redis for live updates and caching, and background job workers
- The web app published on port
80; all state, the SQLite database and every uploaded attachment, held in one Docker volume campfire.serviceandcampfire-firstboot.serviceas systemd units, enabled and active on boot- A unique application secret, a unique Web Push key pair and a single administrator account with a random password, all generated per VM on first boot and never baked into the image
- No default login, no shipped secret, and no open first run wizard for a passer by to claim
- Ubuntu 24.04 LTS base with latest security patches applied at build time
- Azure Linux Agent for seamless cloud integration and SSH key injection
- 24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
- Active Azure subscription, SSH public key, VNet + subnet in target region
- Subscription to the Campfire listing on Azure Marketplace
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is a sensible starting point for a small team. For a larger team or heavy attachment traffic use Standard_B2ms or Standard_D2s_v5. NSG inbound: allow 22/tcp from your management network and 80/tcp for the web app from the networks that use it.
Step 1: Deploy from the Azure Portal
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Campfire by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create.
Step 2: Deploy from the Azure CLI
RG="campfire-prod"; LOCATION="eastus"; VM_NAME="campfire-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/campfire-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name cf-vnet --address-prefix 10.100.0.0/16 --subnet-name cf-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name cf-nsg
az network nsg rule create -g "$RG" --nsg-name cf-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name cf-nsg --name allow-http --priority 110 \
--destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name cf-vnet --subnet cf-subnet --nsg cf-nsg --public-ip-sku Standard
Step 3: Connect to your VM
ssh azureuser@<vm-ip>
Step 4: Confirm the services are running
Campfire runs as one container under campfire.service. That single container is a complete deployment in itself: Thruster fronts Puma running the Rails application, a bundled Redis carries live updates and the cache, and background workers deliver notifications. Only port 80 is published. Confirm the services are active and see the container:
sudo systemctl is-active docker campfire-firstboot campfire
sudo docker compose -f /var/lib/campfire/docker-compose.yml ps
All three services report active and the campfire container reports Up, with 0.0.0.0:80->80/tcp published. campfire-firstboot.service is a one shot unit that has already finished; it stays active because it remains after exit, and it will not run again on this VM.
Step 5: Read the per instance credentials
On this VM's first boot, before the application was reachable, a unique application secret and Web Push key pair were generated, and a single administrator account was created with a random password and written to a root only file. Read it:
sudo cat /root/campfire-credentials.txt
sudo stat -c '%a %U:%G %n' /root/campfire-credentials.txt
The file (mode 0600 root:root) holds CAMPFIRE_ADMIN_EMAIL and CAMPFIRE_ADMIN_PASSWORD, the account you sign in to the web app with, plus the app url. None of these ship in the image; every value is unique to this VM, so no two instances share a credential and there is no default login to change.

Step 6: Verify the security model
Campfire's health endpoint is deliberately open so a load balancer can probe it, but every other route requires a signed in session, and the first run wizard that would let a visitor claim the instance is already closed because first boot created the administrator. Confirm all four:
B=http://localhost
curl -s -o /dev/null -w 'GET /up (no auth): %{http_code}\n' $B/up
curl -s -o /dev/null -w 'GET /first_run (no auth): %{http_code}\n' $B/first_run
curl -s -o /dev/null -w 'GET /rooms/1 (no auth): %{http_code}\n' $B/rooms/1
curl -s -o /dev/null -w 'POST /rooms/1/messages (no auth): %{http_code}\n' -X POST $B/rooms/1/messages
/up returns 200. /first_run returns 302: the wizard redirects away because an account already exists, so nobody can claim this instance. Reading a room and posting into a room both return 302 to the sign in page for an anonymous visitor. That is the security model in four lines: one open health probe, everything else behind a session.

Step 7: Sign in from the command line
Prove the per instance credential authenticates before you open a browser. Campfire uses Rails' per form CSRF tokens, so take the session wide token from the page's csrf-token meta tag:
B=http://localhost
J=$(mktemp)
TOK=$(curl -s -c $J -b $J $B/session/new | grep -oE 'csrf-token" content="[^"]+"' | head -1 | sed -E 's/.*content="([^"]+)".*/\1/')
curl -s -c $J -b $J -o /dev/null -w 'sign in (per VM admin): %{http_code}\n' \
--data-urlencode "authenticity_token=$TOK" \
--data-urlencode "email_address=<CAMPFIRE_ADMIN_EMAIL>" \
--data-urlencode "password=<CAMPFIRE_ADMIN_PASSWORD>" \
-X POST $B/session
curl -s -b $J -c $J -o /dev/null -w 'landing page (session): %{http_code}\n' $B/
rm -f $J
The sign in returns 302 (Campfire redirects you into your last room) and the landing page then returns 302 as well, redirecting to the room you belong to. A wrong password returns 401 instead.
Step 8: Open the web app and sign in
Browse to http://<vm-ip>/ to reach Campfire. Sign in with the email address and password from Step 5. The email address is shown on the sign in page by upstream design, so that anyone who forgets their password knows who to ask.

Step 9: Talk in a room
You land in the first room, called All Talk, which every member of the account belongs to automatically. Type in the composer at the bottom and press enter. Messages appear live for everyone in the room without a refresh, the paperclip button attaches a file, and the A button turns on rich text. The right hand rail lists everyone in the account at the top and every room below it, and the + button creates a new room.

Step 10: Invite your team and manage the account
Open the settings gear in the bottom right corner to reach Account settings. From here you rename the account, upload a logo, decide whether ordinary members may create rooms, and copy the invite link. Anyone who opens that link creates their own account on this instance, so treat it like a password and regenerate it with the refresh button if it leaks. The member list below lets you promote someone to administrator with the crown button, or remove them.

Step 11: Search the whole history
Click the magnifying glass to the left of the composer and type. Campfire searches every message you can see across every room, and each hit shows the author, the time and the room it came from, so you can jump straight to it.

Step 12: Confirm which build you are running
The image reference is pinned by tag and by digest, so an upgrade is always something you choose rather than something that happens to you. Confirm what is actually running:
sudo docker compose -f /var/lib/campfire/docker-compose.yml images
sudo docker image inspect --format '{{index .RepoDigests 0}}' \
"$(sudo docker compose -f /var/lib/campfire/docker-compose.yml images -q | head -1)"
Both the tag and the sha256: digest are shown. To move to a newer upstream release, edit the image: line in /var/lib/campfire/docker-compose.yml to the tag and digest you want, then restart the service. Campfire runs any pending database migrations automatically when the container starts.

Step 13: Serve HTTPS with your own domain
The image ships plain HTTP on port 80 so the VM is reachable on its IP address the moment it boots. Campfire can obtain and renew its own Let's Encrypt certificate instead. Point a DNS name at the VM, open port 443 in the network security group, then edit /var/lib/campfire/.env: remove the DISABLE_SSL line, add your domain, publish 443 in the compose file, and restart.
# /var/lib/campfire/.env
# remove this line:
DISABLE_SSL=true
# and add:
TLS_DOMAIN=chat.example.com
# /var/lib/campfire/docker-compose.yml — publish 443 alongside 80:
ports:
- "80:80"
- "443:443"
# then:
sudo systemctl restart campfire.service
Leave DISABLE_SSL=true in place if you terminate TLS in something in front of the VM, such as an Azure Application Gateway or your own reverse proxy. Removing it without a resolvable TLS_DOMAIN will make the app redirect every plain HTTP request to HTTPS and it will stop answering on its IP address.
Step 14: Back up your conversation
Everything Campfire keeps lives in one Docker volume: the SQLite database and every uploaded file. Because the database can be written to at any moment, take a consistent snapshot first rather than copying the file directly:
sudo docker exec campfire script/admin/prepare-backup
sudo docker exec campfire ls -la storage/backups
That writes a consistent snapshot into storage/backups inside the volume. Archive the whole volume to a file on the host and copy it somewhere safe, ideally off the machine, on whatever schedule suits you. Azure disk snapshots of the OS disk are a reasonable complement, but take the database snapshot first so the copy is consistent.
Troubleshooting
The web app does not answer. Check the container and the last log lines:
sudo systemctl status campfire.service --no-pager | head -20
sudo docker compose -f /var/lib/campfire/docker-compose.yml logs --tail 40 campfire
You lost the administrator password. Reset it from inside the container, replacing the address and the new password with your own:
sudo docker exec campfire script/admin/reset-password admin@example.com 'a-new-strong-password'
Everything redirects to https and the site will not load on the IP. DISABLE_SSL has been removed from /var/lib/campfire/.env without a working TLS_DOMAIN. Put it back and restart the service.
Support
cloudimg provides 24/7 support with a guaranteed 24 hour response SLA for this image. Contact us at support@cloudimg.co.uk or visit www.cloudimg.co.uk.