Self-hosting

Run your own Campsend.

Campsend is open source under the MIT license. You run it on one server with your own object storage and your own SMTP service. A self-hosted installation has no storage limit and no delivery limit.

View the repository Don't want to run it yourself? Use the hosted version.

Before you start

Campsend is one Rails application backed by SQLite. It runs as a single container with the databases on a volume, so there's no database server to operate.

You'll need:

  • A server with Docker. One host. Campsend isn't built to run more than one writable copy at a time.
  • An SMTP service. Campsend emails sign-in links and delivery links. Without working email nobody can sign in.
  • Somewhere to keep files. A persistent volume on the server, or a private S3-compatible bucket.
  • A hostname with TLS. Kamal gets the certificate for you.

To work on Campsend locally you'll need Ruby 3.3.4 and SQLite 3 instead.

Running it locally

Start here before you deploy. The local setup uses SQLite and writes email to a file, so you can see the whole product working without any credentials.

git clone https://github.com/obakeng-develops/campsend.git
cd campsend
bin/setup
bin/dev

bin/setup installs the gems and prepares the SQLite databases. bin/dev starts the server on localhost:3000.

Then send your first delivery:

  1. Signing in

    Enter any email address at /session/new. Campsend writes the email to tmp/mails instead of sending it. Open that file and follow the link inside.

  2. Sending a file

    Pick a file, enter a recipient address and a note, then send. The delivery email lands in tmp/mails too.

  3. Checking what was recorded

    Follow the delivery link and download the file. Your Sent page now shows when the delivery was opened and when it was downloaded.

🚨 NB: sign-in links are single use and last 15 minutes. If you open one twice, the second attempt tells you it expired.

Choosing where files live

Campsend writes uploaded files to a volume on the server or to a private S3-compatible bucket. Pick one before you deploy. Changing ACTIVE_STORAGE_SERVICE later doesn't move anything, so files stored under the old service stop resolving.

Storage provider

Files sit on the persistent volume at /rails/storage, next to the databases. There's nothing else to configure and the backups you already take cover them.

ACTIVE_STORAGE_SERVICE=local

Keeps files off the application volume, so you can rebuild the server without moving data.

ACTIVE_STORAGE_SERVICE=s3
STORAGE_ENDPOINT=https://s3.us-east-1.amazonaws.com
STORAGE_REGION=us-east-1
STORAGE_BUCKET=campsend
STORAGE_ACCESS_KEY_ID=...
STORAGE_SECRET_ACCESS_KEY=...
STORAGE_FORCE_PATH_STYLE=false

R2 uses the region auto and path-style requests. If presigned upload URLs come from a different origin than STORAGE_ENDPOINT, also set STORAGE_BROWSER_ORIGIN to that origin.

ACTIVE_STORAGE_SERVICE=s3
STORAGE_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com
STORAGE_REGION=auto
STORAGE_BUCKET=campsend
STORAGE_ACCESS_KEY_ID=...
STORAGE_SECRET_ACCESS_KEY=...
STORAGE_FORCE_PATH_STYLE=true

Use the S3-compatible endpoint for your bucket's region. The native B2 API endpoint won't work.

ACTIVE_STORAGE_SERVICE=s3
STORAGE_ENDPOINT=https://s3.us-west-004.backblazeb2.com
STORAGE_REGION=us-west-004
STORAGE_BUCKET=campsend
STORAGE_ACCESS_KEY_ID=...
STORAGE_SECRET_ACCESS_KEY=...
STORAGE_FORCE_PATH_STYLE=true

Securing the bucket

Keep the bucket private. Give the credentials read, write and delete on that one bucket and nothing else. Campsend never needs account administration.

Browsers upload straight to the bucket, so it needs a CORS rule. Replace the origin with your own:

[
  {
    "AllowedOrigins": ["https://campsend.example.com"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["Content-Type", "Content-MD5", "Content-Disposition"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

Configuring email

Campsend sends sign-in links and delivery notifications over SMTP. Point it at any SMTP service.

APP_HOST=campsend.example.com
[email protected]
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=...
SMTP_PASSWORD=...

APP_HOST is the hostname on its own, without https://. Campsend uses it to build links and to authorize incoming requests, so a wrong value locks you out of your own installation.

🚨 NB: set up SPF and DKIM on the sending domain. If sign-in email goes to spam, nobody can sign in.

Deploying with Kamal

Campsend ships with a Kamal configuration. Kamal builds the image, pushes it to your registry and runs it on your server behind a proxy that terminates TLS.

  1. Filling in the configuration

    Edit config/deploy.yml with your server address, image registry and hostname. Put every secret in .kamal/secrets, which is gitignored.

  2. Generating a master key

    Each installation needs its own RAILS_MASTER_KEY. Don't commit it, and don't reuse one from another deployment.

  3. Running setup once

    Run bin/kamal setup. It provisions the server, mounts the volume and starts Campsend. Every release after that needs bin/kamal deploy.

bin/kamal setup      # first time
bin/kamal deploy     # every release after that

The container entrypoint migrates the database before Puma starts, so you don't run migrations yourself.

Verifying the deployment

  1. /up returns HTTP 200.
  2. A sign-in email arrives and the link works once.
  3. A small file uploads and appears in My Files.
  4. The recipient signs in and sees the delivery under Shared with me.
  5. Downloading the file records the event on your Sent page.
  6. Your backups include the persistent volume.

Keeping it running

Campsend keeps SQLite on a volume mounted at /rails/storage. That volume holds the application, cache, queue and cable databases. Run one writable Campsend host against it.

Keep Solid Queue running or scheduled deliveries never publish. The default deployment runs it inside Puma with SOLID_QUEUE_IN_PUMA=true. Split it into its own job process before you add a second web host.

Back the databases up with a SQLite-aware snapshot, not a file copy of a database that's being written to. Test a restore. Watch free disk space.

Campsend writes one JSON event per request and per job to standard output. Each line carries the request ID, route, status, duration and the records involved. Email addresses, access tokens and file names are left out. Ship those lines wherever you keep logs and you can answer most questions from a single row.

Configuration reference

Campsend reads production configuration from environment variables. Production boot stops when a required setting is missing.

These are the variables you'll set:

VariableRequiredWhat it does
APP_HOSTYesPublic hostname, no protocol. Used in links and host authorization.
ACTIVE_STORAGE_SERVICEYeslocal or s3.
RAILS_MASTER_KEYYesDecrypts the credentials file. Unique per installation.
MAIL_FROMYesSender address on Campsend email.
SMTP_ADDRESSYesSMTP server hostname.
SMTP_USERNAMEYesSMTP username.
SMTP_PASSWORDYesSMTP password.
SMTP_PORTNoDefaults to 587.
SOLID_QUEUE_IN_PUMANoRuns the job queue inside the web process. Right for one host.
WEB_CONCURRENCYNoPuma processes. Defaults to 1.
JOB_CONCURRENCYNoSolid Queue workers. Defaults to 1.
RAILS_LOG_LEVELNoDefaults to info.

Storage variables are in Choosing where files live. The repository has the full reference, including every S3 and Google Drive setting.

Read the configuration reference

Fixed limits

These are set in the code and are the same in every installation.

Sign-in link15 minutes, single use
Sender session30 days
Delivery link30 days
Signed storage URL5 minutes
Files in one delivery20
Size of one delivery2 GB

Enabling Google Drive imports

Google Drive imports are off unless you set all three variables. Leave them unset and nothing about the feature appears in the interface.

To turn it on, create a Google Cloud project, enable the Picker and Drive APIs, then configure the consent screen with the drive.file scope. Create an OAuth web client that lists your Campsend origin, and an API key restricted to that origin and the Picker API. Then set GOOGLE_DRIVE_CLIENT_ID, GOOGLE_DRIVE_API_KEY and GOOGLE_DRIVE_APP_ID.

All three are browser-public configuration. You restrict them in Google Cloud rather than treating them as server secrets, so don't add a client secret. Campsend uses a short-lived browser token for each import and stores no Google refresh token, so it reads only the files someone picks in Picker.

What you get

A self-hosted Campsend has no storage limit and no delivery limit. Plans don't exist in this code. Quotas and paid tiers live in a separate distribution that extends Campsend through one hook, so nothing here is turned off to sell you an upgrade.

You get delivery branding, return uploads from recipients, scheduled deliveries, file revisions, collections and open and download tracking.

The hook is CAMPSEND_EXTENSIONS_GEMFILE, which loads an extra Gemfile at bundle time. Extensions run with the same privileges as Campsend, so only load code you trust.

Questions

  • Is SQLite enough?

    For one host, yes. A local stress test held 670 to 860 requests a second on reads, with p99 under 200ms at 100 concurrent connections, and ran 37,000 requests over 60 seconds with no failures. Ruby rendering reaches its limit before SQLite does. Move to a server database when you need a second application host.

  • Can I run more than one web host?

    Not against the same SQLite volume. Split Solid Queue into its own process and move to a server database first.

  • How do I move files from local disk to a bucket?

    Changing ACTIVE_STORAGE_SERVICE doesn't move anything, so files stored under the old service stop resolving. Read the security and storage explanation in the repository before you migrate objects.

  • Do I need libvips or a JavaScript build step?

    No. Campsend uses importmaps, so there's no bundler to run, and image variants are turned off.

  • Something's wrong with the docs or the code.

    Open an issue on the repository. Pull requests are welcome.

Clone it and run it.

View on GitHub