Supabase, the open-source Firebase alternative, is surprisingly easy to self-host with Docker, letting you ditch vendor lock-in and gain complete control over your data and infrastructure.

Here’s a look at Supabase running in a typical self-hosted setup. This is a docker-compose.yml file defining the services:

version: '3.8'

services:
  db:
    image: supabase/postgres:14.5.0.77
    container_name: supabase_db
    ports:
      - "5432:5432"
    volumes:
      - supabase_db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: your_postgres_password # Change this!
      POSTGRES_USER: supabase_user
      POSTGRES_DB: supabase_db
    networks:
      - supabase_network

  studio:
    image: supabase/studio:20230801.80.ga96f15c
    container_name: supabase_studio
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      POSTGRES_PASSWORD: your_postgres_password # Change this!
      POSTGRES_USER: supabase_user
      POSTGRES_DB: supabase_db
      POSTGRES_HOST: db
      POSTGRES_PORT: 5432
      JWT_SECRET: your_jwt_secret # Change this!
      DEFAULT_MAX_ROWS_PER_PAGE: 1000
    networks:
      - supabase_network

  gotrue:
    image: supabase/gotrue:v2.15.1
    container_name: supabase_gotrue
    ports:
      - "8000:8000"
    depends_on:
      - db
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: supabase_user
      DB_PASSWORD: your_postgres_password # Change this!
      DB_NAME: supabase_db
      JWT_SECRET: your_jwt_secret # Change this!
      SITE_URL: http://localhost:3000 # Adjust if needed
      API_URL: http://localhost:8000 # Adjust if needed
      LOG_LEVEL: info
    networks:
      - supabase_network

  realtime:
    image: supabase/realtime:v0.34.2
    container_name: supabase_realtime
    ports:
      - "4000:4000"
    depends_on:
      - db
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: supabase_user
      DB_PASSWORD: your_postgres_password # Change this!
      DB_NAME: supabase_db
      JWT_SECRET: your_jwt_secret # Change this!
      PORT: 4000
    networks:
      - supabase_network

  storage:
    image: supabase/storage:0.2.16
    container_name: supabase_storage
    ports:
      - "5001:5001"
    depends_on:
      - db
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: supabase_user
      DB_PASSWORD: your_postgres_password # Change this!
      DB_NAME: supabase_db
      JWT_SECRET: your_jwt_secret # Change this!
      LOG_LEVEL: info
      PRIVATE_KEY_PATH: /etc/storage/private.key # Ensure this file exists or is mounted
      PUBLIC_KEY_PATH: /etc/storage/public.key # Ensure this file exists or is mounted
    volumes:
      - ./storage/keys:/etc/storage # Mount keys here
    networks:
      - supabase_network

volumes:
  supabase_db_data:

networks:
  supabase_network:
    driver: bridge

This docker-compose.yml file defines the core Supabase services: db (PostgreSQL), studio (the dashboard), gotrue (authentication), realtime (websockets), and storage (file storage). Each service has its own Docker image, environment variables for configuration (like database credentials and JWT secrets), and network setup.

The db service uses the official PostgreSQL image, exposing port 5432 and persisting data in a Docker volume named supabase_db_data. The studio service runs on port 3000 and depends on the database being available. gotrue handles authentication on port 8000, realtime manages WebSocket connections on port 4000, and storage serves files on port 5001.

Key Configuration Points:

  • Passwords and Secrets: Crucially, you must change your_postgres_password and your_jwt_secret to strong, unique values. These are used for database access and signing JWTs for authentication.
  • Database Connection: All services connect to the db service using the hostname db and the specified port, user, password, and database name.
  • URLs: SITE_URL and API_URL in gotrue should reflect how your Supabase instance will be accessed. For local development, http://localhost:3000 and http://localhost:8000 are common.
  • Storage Keys: The storage service requires public and private keys for secure file operations. You’ll need to generate these (e.g., using openssl) and mount them into the container.

To get this running, first create a directory for your storage keys (e.g., mkdir storage && mkdir storage/keys), generate the keys (you can use openssl genrsa -out storage/keys/private.key 2048 and then openssl rsa -pubout -in storage/keys/private.key -out storage/keys/public.key), and then run:

docker-compose up -d

This will download the necessary Docker images and start all the Supabase services in detached mode. You can then access the Supabase Studio at http://localhost:3000 to manage your database and project.

The real magic of Supabase lies in its auto-generated APIs. Once your database tables are set up (either manually through the Studio or via migrations), Supabase automatically provides RESTful endpoints and a GraphQL API for them. This means you can interact with your data directly from your frontend application without writing any backend API code.

For example, if you have a todos table with id and task columns, you can fetch all todos with a simple GET request to /rest/v1/todos using your Supabase project’s API URL. Authentication is handled via JWTs passed in the Authorization: Bearer <your_jwt> header.

The realtime service enables live subscriptions. You can subscribe to changes in specific tables or rows. For instance, in your client-side code, you might use a Supabase SDK to subscribe to INSERT events on the todos table. When a new todo is added by any user, all subscribed clients will receive the update instantly via WebSockets.

One of the most powerful, yet often overlooked, aspects of Supabase’s self-hosted setup is how it leverages PostgreSQL’s Row Level Security (RLS). While the Studio provides a UI for managing RLS policies, understanding that these policies are applied directly at the database level is key. This means that even if an attacker gains access to your API endpoints, they cannot bypass your defined security rules, as the enforcement happens within PostgreSQL itself. You define policies like CREATE POLICY "Allow logged-in users to read their own posts" ON posts FOR SELECT USING (auth.uid() = user_id);, and Supabase’s gotrue service ensures that the auth.uid() function correctly returns the authenticated user’s ID for enforcement.

The next step after getting Supabase running locally is often setting up a robust deployment strategy for production, including SSL termination and domain configuration.

Want structured learning?

Take the full Supabase course →