Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Fizzy Docker Compose Configuration
# Copy this file to .env and fill in the values for your deployment.
#
# NOTE: SECRET_KEY_BASE, VAPID_PUBLIC_KEY, and VAPID_PRIVATE_KEY are
# automatically generated on first boot and stored in the fizzy_secrets volume.
# Do NOT set them here — the entrypoint sources the secrets file on every boot,
# which will overwrite any values set in this file.

# =============================================================================
# REQUIRED
# =============================================================================

# Public URL of your Fizzy instance (used in email links, webhooks, etc.)
# Include the scheme and host; no trailing slash.
BASE_URL=https://fizzy.example.com

# =============================================================================
# DOCKER IMAGE
# =============================================================================

# Override the GHCR image repo (default: basecamp/fizzy)
# FIZZY_IMAGE_REPO=basecamp/fizzy

# Image tag to deploy (default: latest)
# FIZZY_IMAGE_TAG=latest

# Host port to expose Fizzy on (default: 3006)
# In production, this port should be firewalled; only your reverse proxy should reach it.
# FIZZY_PORT=3006

# =============================================================================
# MULTI-TENANCY
# =============================================================================

# Set to true to allow anyone to create a new account (sign-up enabled).
# Set to false (default) for a single-account / invite-only deployment.
MULTI_TENANT=false

# =============================================================================
# EMAIL (SMTP)
# =============================================================================

# If SMTP_ADDRESS is not set, email delivery is disabled entirely.
# Required for magic-link login and notification emails.

# SMTP_ADDRESS=smtp.example.com
# SMTP_PORT=587
# SMTP_DOMAIN=example.com
# SMTP_USERNAME=user@example.com
# SMTP_PASSWORD=secret
# SMTP_AUTHENTICATION=plain
# SMTP_TLS=false
# SMTP_SSL_VERIFY_MODE=

# From address for outgoing emails
# MAILER_FROM_ADDRESS=fizzy@example.com

# =============================================================================
# LOGGING
# =============================================================================

# Log level: debug, info, warn, error, fatal (default: info)
# RAILS_LOG_LEVEL=info

# =============================================================================
# PERFORMANCE TUNING
# =============================================================================

# Number of Puma web worker processes (default: physical CPU count)
# WEB_CONCURRENCY=2

# Number of Solid Queue job worker processes (default: physical CPU count)
# JOB_CONCURRENCY=1
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Ignore all environment files (except templates).
/.env*
!/.env*.erb
!/.env.example

# Ignore all logfiles and tempfiles.
/log/*
Expand Down Expand Up @@ -41,3 +42,6 @@

/config/credentials/*.key
.DS_Store

# Git worktrees
/.worktrees
28 changes: 27 additions & 1 deletion bin/docker-entrypoint
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
#!/bin/bash -e

SECRETS_FILE="/rails/secrets/secrets.env"
mkdir -p "$(dirname "$SECRETS_FILE")"

# On first boot: generate SECRET_KEY_BASE and VAPID keys, persist to secrets file
if [ ! -f "$SECRETS_FILE" ]; then
echo "==> First boot detected: generating SECRET_KEY_BASE and VAPID keys..."

SECRET_KEY_BASE=$(bundle exec rails secret) || { echo "ERROR: Failed to generate SECRET_KEY_BASE"; exit 1; }

VAPID_KEYS=$(bundle exec ruby -e "require 'web-push'; vapid = WebPush.generate_key; puts 'VAPID_PUBLIC_KEY=' + vapid.public_key; puts 'VAPID_PRIVATE_KEY=' + vapid.private_key") || { echo "ERROR: Failed to generate VAPID keys"; exit 1; }

TMPFILE=$(mktemp)
{
echo "SECRET_KEY_BASE=${SECRET_KEY_BASE}"
echo "${VAPID_KEYS}"
} > "$TMPFILE" && mv "$TMPFILE" "$SECRETS_FILE" || { echo "ERROR: Failed to write secrets file"; rm -f "$TMPFILE"; exit 1; }

echo "==> Secrets written to ${SECRETS_FILE}. Back up the fizzy_secrets volume to preserve these."
fi

# On every boot: load the secrets file so all vars are exported into the environment
set -a
# shellcheck source=/dev/null
source "$SECRETS_FILE"
set +a

# If running the rails server then create or migrate existing database
if [ "${1}" == "./bin/thrust" ] && [ "${2}" == "./bin/rails" ] && [ "${3}" == "server" ]; then
MIGRATE=1 ./bin/rails db:prepare
fi

exec "${@}"
exec "$@"
58 changes: 58 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# docker-compose.yml
# Production deployment for Fizzy (SQLite + Solid Queue embedded in Puma)
#
# The first boot auto-generates SECRET_KEY_BASE and VAPID keys and writes them
# to the fizzy_secrets volume. Keep that volume backed up.

services:
fizzy:
image: ghcr.io/${FIZZY_IMAGE_REPO:-basecamp/fizzy}:${FIZZY_IMAGE_TAG:-latest}
# To use a locally built image instead, comment out 'image:' above and uncomment:
# build: .
restart: unless-stopped
ports:
- "${FIZZY_PORT:-3006}:80"
volumes:
# SQLite databases and Active Storage file uploads
- fizzy_storage:/rails/storage
# Generated secrets.env (SECRET_KEY_BASE, VAPID keys) live here
# Uses a dedicated /rails/secrets path to avoid overwriting baked-in config files
# IMPORTANT: back up this volume — losing it invalidates all sessions and web push subscriptions
- fizzy_secrets:/rails/secrets
environment:
Comment on lines +18 to +22
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mounting a named volume at /rails/secrets can be unwritable for the non-root rails user if the mountpoint directory doesn’t exist in the image at build time (Docker will create it as root-owned). Ensure /rails/secrets exists and is owned by uid 1000 in the image so the first-boot secret generation can write secrets.env.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only got'ja that is left in the build. You have to chown the mount volume once they're created. But I'm used to that with self hosting. Could just switch it to use root, instead. Personally, I'd just leave it. YMMV.

# .env.example has some examples for what needs to be in place for this.
- RAILS_ENV=$RAILS_ENV
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RAILS_ENV=$RAILS_ENV will override the image’s RAILS_ENV=production with an empty string when RAILS_ENV isn’t set in the host/.env, which can cause Rails to boot in the wrong environment. Consider removing this line (let the image default stand) or using a default like production in the compose interpolation.

Suggested change
- RAILS_ENV=$RAILS_ENV
- RAILS_ENV=${RAILS_ENV:-production}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, but that should be part of your environment. So I don't see a need to update it. Up to ya'll if you want me to add this.

- FIZZY_HOST=$FIZZY_HOST
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIZZY_HOST is passed into the container here, but it doesn’t appear to be referenced anywhere in the codebase. Keeping unused operator-facing config increases confusion; consider removing it from compose (and .env.example if added later) or wiring it to an actual setting.

Suggested change
- FIZZY_HOST=$FIZZY_HOST

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fair. I left it because others had it. I am happy to remove it for whoever reviews this, as needed.

- SMTP_ADDRESS=$SMTP_ADDRESS
- SMTP_PORT=$SMTP_PORT
- SMTP_DOMAIN=$SMTP_DOMAIN
- SMTP_USERNAME=$SMTP_USERNAME
- SMTP_PASSWORD=$SMTP_PASSWORD
- SMTP_AUTHENTICATION=$SMTP_AUTHENTICATION
- SMTP_TLS=$SMTP_TLS
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.example documents SMTP_SSL_VERIFY_MODE, and production.rb reads it, but docker-compose.yml never passes it into the container. As a result operators can’t configure SSL verification mode when using compose; add it to the environment: list (or switch to env_file:).

Suggested change
- SMTP_TLS=$SMTP_TLS
- SMTP_TLS=$SMTP_TLS
- SMTP_SSL_VERIFY_MODE=$SMTP_SSL_VERIFY_MODE

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. I didn't add it to mine, because I didn't need it, and I don't think most people will need it. I left it in as part of the examples though.

- MAILER_FROM_ADDRESS=$MAILER_FROM_ADDRESS
- RAILS_LOG_LEVEL=$RAILS_LOG_LEVEL
- BASE_URL=$BASE_URL
- WEB_CONCURRENCY=2
- JOB_CONCURRENCY=1
- MULTI_TENANT=true
Comment on lines +36 to +38
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WEB_CONCURRENCY, JOB_CONCURRENCY, and MULTI_TENANT are hard-coded here, which prevents operators from changing them via .env (and MULTI_TENANT=true conflicts with .env.example/config/deploy.yml defaulting it to false). Prefer wiring these from env with sensible defaults so compose behaves as documented.

Suggested change
- WEB_CONCURRENCY=2
- JOB_CONCURRENCY=1
- MULTI_TENANT=true
- WEB_CONCURRENCY=${WEB_CONCURRENCY:-2}
- JOB_CONCURRENCY=${JOB_CONCURRENCY:-1}
- MULTI_TENANT=${MULTI_TENANT:-false}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they could be moved to the .env.example file, but I didn't see any good reason to do so. So I hardcoded them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an interesting discussion why one might want to change them: #2350 (comment)

# Disable Rails' own SSL enforcement — TLS is terminated by the external reverse proxy
- DISABLE_SSL=true
# Solid Queue embedded in Puma (no separate worker container needed)
- SOLID_QUEUE_IN_PUMA=true
# Use local disk for Active Storage (files go into fizzy_storage volume)
- ACTIVE_STORAGE_SERVICE=local
# SQLite adapter
- DATABASE_ADAPTER=sqlite
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/up"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s

volumes:
fizzy_storage:
driver: local
fizzy_secrets:
driver: local
Loading