diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..4e4d305908 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 38bd80d0df..d44310c643 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Ignore all environment files (except templates). /.env* !/.env*.erb +!/.env.example # Ignore all logfiles and tempfiles. /log/* @@ -41,3 +42,6 @@ /config/credentials/*.key .DS_Store + +# Git worktrees +/.worktrees diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index ad59377a4e..146d93c52a 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -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 "$@" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..1d98ccb719 --- /dev/null +++ b/docker-compose.yml @@ -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: + # .env.example has some examples for what needs to be in place for this. + - RAILS_ENV=$RAILS_ENV + - FIZZY_HOST=$FIZZY_HOST + - 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 + - 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 + # 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