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
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"Create the .env file
Compose reads
.envfor its own variables and passes it to the SpecsGraph containers. SetSPECSGRAPH_PUBLIC_URLto the exact URL people will type, with no trailing slash. PinSPECSGRAPH_VERSIONto 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:587Generate 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 .envCreate your GitHub App
Follow Create your GitHub App. Fill
GITHUB_APP_ID,GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETin.env, and copy the downloaded private key to./secrets/github-app.pemon 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.pemAdd the Caddyfile
Save the Caddyfile from the section below as
Caddyfilenext to the compose file, with your own domain on the first line.Start the stack
Pull the images and start every service in the background.
Shell docker compose up -dWatch the logs
The first start takes a little longer: Postgres creates its database, the
apiservice 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 caddySign in as the first user
Open
https://specsgraph.example.comand 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.
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.pemThe 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.
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
}
}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:
Troubleshooting covers problems after the install, and where to get help.
Next steps
- Connect GitHub: link your repositories to a project.
- Connect an agent: point Claude Code, Cursor or another client at your MCP URL.
- Upgrades and backups: schedule database backups before the team moves in.