Skip to content

Install with Docker Compose

Install self-hosted SpecsGraph with Docker Compose: get the compose file, generate secrets, add Caddy for automatic TLS and sign in as the first user.

Before you start

  • A Linux server that meets the minimum requirements, with Docker Engine and the Compose v2 plugin.
  • A DNS record for your domain pointing at the server. The examples use specsgraph.example.com.
  • Ports 80 and 443 open to the internet, so Caddy can get a certificate and GitHub can deliver webhooks.
  • Owner rights in the GitHub organization that holds your repositories, to create the GitHub App.

The commands below assume the install lives in /opt/specsgraph. Any directory works; keep the compose file, .env, the Caddyfile and the secrets folder together.

Install step by step

  1. Get the compose file

    Create the install directory and download the compose file (opens in a new tab) of the release you will run. The example downloads 1.0.0; use the same version in the next step. The file matches the example further down this page; when the two differ, the file in the repository for your release wins.

    Shell
    sudo mkdir -p /opt/specsgraph && sudo chown "$USER" /opt/specsgraph
    cd /opt/specsgraph
    curl -fsSL -o docker-compose.yml "https://github.com/specsgraph/specsgraph/raw/v1.0.0/deploy/docker-compose.yml"
  2. Create the .env file

    Compose reads .env for its own variables and passes it to the SpecsGraph containers. Set SPECSGRAPH_PUBLIC_URL to the exact URL people will type, with no trailing slash. Pin SPECSGRAPH_VERSION to the release whose compose file you downloaded; the releases page (opens in a new tab) lists them. The three GitHub values stay empty for now.

    .envEnv
    # Compose settings
    SPECSGRAPH_VERSION=1.0.0
    
    # SpecsGraph
    SPECSGRAPH_PUBLIC_URL=https://specsgraph.example.com
    GITHUB_APP_PRIVATE_KEY_PATH=/run/secrets/github-app.pem
    LOG_LEVEL=info
    
    # Filled in after you create the GitHub App
    GITHUB_APP_ID=
    GITHUB_CLIENT_ID=
    GITHUB_CLIENT_SECRET=
    
    # Optional: invitations and review notifications by email
    # SMTP_URL=smtp://user:pass@smtp.example.com:587
  3. Generate secrets

    Append the database password, the secret key and the webhook secret with openssl. Hex output is safe inside the database URL. Do this before the first start: Postgres sets its password only when it creates the data volume.

    Shell
    cat >> .env <<EOF
    POSTGRES_PASSWORD=$(openssl rand -hex 24)
    SPECSGRAPH_SECRET_KEY=$(openssl rand -hex 32)
    GITHUB_APP_WEBHOOK_SECRET=$(openssl rand -hex 20)
    EOF
    chmod 600 .env
    
    # Print the webhook secret: you paste it into GitHub in the next step
    grep GITHUB_APP_WEBHOOK_SECRET .env
  4. Create your GitHub App

    Follow Create your GitHub App. Fill GITHUB_APP_ID, GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in .env, and copy the downloaded private key to ./secrets/github-app.pem on the server.

    Shell
    mkdir -p secrets && chmod 700 secrets
    # then copy the .pem file from your computer, for example:
    # scp ~/Downloads/your-app.private-key.pem server:/opt/specsgraph/secrets/github-app.pem
  5. Add the Caddyfile

    Save the Caddyfile from the section below as Caddyfile next to the compose file, with your own domain on the first line.

  6. Start the stack

    Pull the images and start every service in the background.

    Shell
    docker compose up -d
  7. Watch the logs

    The first start takes a little longer: Postgres creates its database, the api service applies migrations and Caddy requests a certificate. Press Ctrl+C to stop following the logs; the containers keep running.

    Shell
    docker compose logs -f web api mcp caddy
  8. Sign in as the first user

    Open https://specsgraph.example.com and sign in with GitHub. The first person to sign in becomes the Owner. Then create your workspace and project as in the Quickstart, from step 3 on. To bring in a workspace exported from another install, choose Import workspace instead of creating one.

Warning

Claim the install right away

Until someone signs in, anyone who can reach the URL can become Owner. Keep the server behind a firewall or VPN until then, or sign in as soon as the stack is up, before you share the address.

Once the install has an Owner, other people join the workspace through invitations from an Admin or Owner, as described in Members and roles.

The compose file

This is a complete docker-compose.yml for one server. The three SpecsGraph services share their settings through the x-specsgraph block. Compose builds DATABASE_URL from POSTGRES_PASSWORD, so the password lives in one place. Postgres and Redis have health checks, and SpecsGraph starts only once both report healthy.

docker-compose.ymlYAML
name: specsgraph

# Settings shared by the three SpecsGraph services.
x-specsgraph: &specsgraph
  restart: unless-stopped
  env_file: .env
  environment:
    DATABASE_URL: postgres://specsgraph:${POSTGRES_PASSWORD}@postgres:5432/specsgraph
    REDIS_URL: redis://redis:6379
  secrets:
    - github-app.pem
  depends_on:
    postgres:
      condition: service_healthy
    redis:
      condition: service_healthy
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "5"

services:
  web:
    <<: *specsgraph
    image: ghcr.io/specsgraph/specsgraph-web:${SPECSGRAPH_VERSION}
    expose:
      - "3000"

  api:
    <<: *specsgraph
    image: ghcr.io/specsgraph/specsgraph-api:${SPECSGRAPH_VERSION}
    expose:
      - "8080"

  mcp:
    <<: *specsgraph
    image: ghcr.io/specsgraph/specsgraph-mcp:${SPECSGRAPH_VERSION}
    expose:
      - "8090"

  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: specsgraph
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: specsgraph
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U specsgraph -d specsgraph"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7
    restart: unless-stopped
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - web
      - api
      - mcp

volumes:
  postgres-data:
  redis-data:
  caddy-data:
  caddy-config:

secrets:
  github-app.pem:
    file: ./secrets/github-app.pem

The private key reaches the containers as a Compose secret, mounted read-only at /run/secrets/github-app.pem. Data lives in named volumes: postgres-data is the one that matters, and the backup guide covers it. The logging options rotate the logs of the three SpecsGraph services so they cannot fill the disk.

Reverse proxy with Caddy

Caddy gets and renews a TLS certificate for your domain automatically, as long as the DNS record points at the server and ports 80 and 443 are reachable. It stores certificates in the caddy-data volume; keep that volume so restarts do not request new ones.

CaddyfileText
specsgraph.example.com {
  # API, GitHub sign-in callback and GitHub webhooks
  handle /api/* {
    reverse_proxy api:8080
  }

  # MCP endpoint for agents: stream responses without buffering
  handle /mcp* {
    reverse_proxy mcp:8090 {
      flush_interval -1
    }
  }

  # Everything else is the web app
  handle {
    reverse_proxy web:3000
  }
}
PathServiceUsed by
/api/*api on port 8080The web app, the GitHub sign-in callback and GitHub webhooks
/mcp*mcp on port 8090Agents connected over MCP
Everything elseweb on port 3000People in the browser

flush_interval -1 sends MCP responses to the agent as they are produced. If you use another proxy, see Configuration for the settings it needs.

Check the install

Run docker compose ps. All six services should be running, with postgres and redis marked healthy. If something is wrong, the logs usually say which of these it is:

SymptomLikely cause
Caddy logs certificate errorsThe DNS record does not point at the server yet, or ports 80 and 443 are closed.
GitHub says the redirect_uri is not associated with this applicationThe callback URL in the App does not match https://specsgraph.example.com/api/auth/github/callback exactly.
The api service cannot authenticate to PostgresPOSTGRES_PASSWORD changed after the first start. Postgres keeps the password it was created with.
Webhook deliveries fail in the App settingsGITHUB_APP_WEBHOOK_SECRET does not match the secret in GitHub, or GitHub cannot reach the server.
Agents connect but time outA proxy in front of Caddy buffers responses. See Configuration.

Troubleshooting covers problems after the install, and where to get help.

Next steps