Skip to content

Docker quick start

The fastest way to run BottleCRM locally is Docker Compose. It brings up PostgreSQL, Redis, the Django API, two Celery processes and the SvelteKit frontend from one file, using the checked-in .env.docker for configuration.

Prerequisites

  • Docker Engine with the Compose v2 plugin (the docker compose subcommand, not the standalone docker-compose binary: docker-compose.yml is written for the plugin).
  • A clone of the repository, with docker-compose.yml at its root.

Clone and configure

All commands below run from the repository root, the directory that contains docker-compose.yml and Dockerfile.

.env.docker is checked into the repository with working development defaults and is loaded by every service in docker-compose.yml. You don't need to copy or edit anything to get a working stack. If you do want to override a value locally (for example to add Google OAuth credentials), create .env.docker.local next to it, docker-compose.yml loads it as an optional, gitignored second env_file whose values win over .env.docker.

The variables .env.docker actually defines are:

Variable Purpose
SECRET_KEY Django secret key. The shipped value is an insecure dev placeholder.
DEBUG True in the dev compose file. This is also what allows devlogin to run (see First sign-in).
ENV_TYPE dev.
ALLOWED_HOSTS localhost,127.0.0.1,backend.
DOMAIN_NAME http://localhost:8000.
DBNAME, DBUSER, DBPASSWORD, DBHOST, DBPORT Connection details the Django backend (and Celery) containers use to reach db.
POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD Used only by the db service itself to create the database and its own superuser role.
CELERY_BROKER_URL, CELERY_RESULT_BACKEND Both point at the redis service, database 0.
DEFAULT_FROM_EMAIL, ADMIN_EMAIL, ADMIN_PASSWORD Console email backend (dev), and the credentials used to bootstrap a Django admin superuser. See What you got.
CORS_ALLOW_ALL True for local development.
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET Blank by default; Google sign-in is disabled until both are set. The redirect URI is not a setting: the frontend sends it in the request body on each call.
PUBLIC_DJANGO_API_URL The API origin the frontend's server-side code calls, http://backend:8000. It uses the compose service name because localhost inside the frontend container is the container itself. Your browser still reaches the API at http://localhost:8000.

Start the stack

docker compose up --build

This builds and starts six services, defined in docker-compose.yml:

Service Image / build Host port
db postgres:16-alpine 5432
redis redis:7-alpine 6379
backend built from the repo root Dockerfile 8000
celery-worker same image as backend, runs celery -A crm worker --loglevel=info .
celery-beat same image as backend, runs celery -A crm beat --loglevel=info ,
frontend built from frontend/Dockerfile 5181

backend, celery-worker and celery-beat all wait for db and redis to report healthy before starting. The backend container's entrypoint (docker/backend/entrypoint.sh) waits for PostgreSQL to accept connections, runs migrate, runs create_default_admin, collects static files, and then starts the Django dev server on 0.0.0.0:8000.

The database role matters. db starts as the postgres superuser (from POSTGRES_USER). That account exists only so the container can create the crm_db database. On first boot, docker/postgres/init-rls-user.sql runs automatically (via Postgres's /docker-entrypoint-initdb.d/ mechanism) and creates a second, non-superuser role, crm_user, granting it the privileges it needs on crm_db. The Django containers connect as crm_user (DBUSER in .env.docker), never as postgres. This is required, not incidental: PostgreSQL Row-Level Security is bypassed entirely for a superuser connection, so if the app ever connected as postgres, every RLS policy in the schema would be silently inert and tenant isolation would depend on application-level filtering alone.

Migrate and seed

entrypoint.sh already runs migrate on every container start, so the schema is up to date as soon as backend reports it's serving. To load demo data, run the seeder inside the running backend container:

docker compose exec backend python manage.py seed_data --email you@example.com

seed_data creates an organization named MicroPyramid (the first organization it creates is always named this, specifically so local-dev workflows have a known, stable name to sign in against) and an admin profile for the user you passed with --email, along with a full set of demo leads, accounts, contacts, opportunities, cases, tasks, invoices and more. Re-running the command reuses the existing MicroPyramid org and admin user rather than creating duplicates.

You can also run migrations manually, the same way you'd run any other management command against the container:

docker compose exec backend python manage.py migrate

Sign in

With DEBUG=True (the default in .env.docker), the fastest way in is devlogin, a management command that mints a JWT pair directly, without going through Google OAuth or email. Full details, including how it resolves --org and what it requires of the target user, are in First sign-in. For the org and user you just seeded:

docker compose exec backend python manage.py devlogin you@example.com --org MicroPyramid

This prints an access token, a refresh token, and the MicroPyramid org's UUID, already bound into the token. See First sign-in for how to use them from the frontend at http://localhost:5181.

What you got

After docker compose up --build and one seed_data run, you have:

  • A PostgreSQL 16 database (crm_db) with RLS-enforcing policies applied by the migrations.
  • A Django API on http://localhost:8000, running under the dev server (not a production WSGI server. This compose file is for development).
  • A Celery worker and a Celery beat scheduler, both connected to the redis service, running the periodic and background tasks registered in backend/crm/celery.py.
  • A SvelteKit frontend on http://localhost:5181, which reaches the API over the compose network at PUBLIC_DJANGO_API_URL=http://backend:8000.
  • A Django admin superuser, created automatically by create_default_admin on first boot, using ADMIN_EMAIL / ADMIN_PASSWORD from .env.docker (admin@localhost / admin unless you override them). This account can sign in to Django's own /admin/ site; it does not have a CRM organization profile, so it can't sign in to the CRM app itself unless you also give it a profile via seed_data or the org-creation flow.
  • If you ran seed_data, an org named MicroPyramid with an admin profile for the email you passed, plus a large set of demo records.

Next steps

  • First sign-in. The three ways to get a session, including the exact devlogin invocation and what Google OAuth and magic links need.
  • Demo data and packs, what seed_data creates in detail, and how vertical packs layer industry-specific pipelines and sample records on top of it.
  • If Docker isn't the right fit, Manual setup covers running the backend, database, Redis, Celery and frontend directly on your machine.