Server Setup
The dashboard is one Go binary and a Postgres database. It serves the web interface, receives uploads from your apps, and holds the socket a live session runs over. There is nothing else to install and no queue or cache to operate.
Prerequisites
Section titled “Prerequisites”- Postgres 16 or later
- Go 1.25 or later, if building from source
With Docker
Section titled “With Docker”git clone https://github.com/getcodescout/code_scout.gitcd code_scoutdocker compose upThat brings up the server and a Postgres instance together. Open http://localhost:24275 and
the first account you create becomes the super admin.
From source
Section titled “From source”git clone https://github.com/getcodescout/code_scout.gitcd code_scoutmake dev-setup # writes .env, creates the local Postgres role and databasemake run # templ watch + air hot reload on :24275make build produces a binary at ./bin/code_scout for deployment.
Configuration
Section titled “Configuration”Boot configuration lives in .env for local development, and in the environment or
/etc/code-scout.conf for a deployed server. It covers the database connection, the address to
listen on and logging: the things that have to be readable before the database is.
Every key has both forms. The environment variable is the TOML key upper-cased with CS_ in
front, so db_host is CS_DB_HOST and conn_max_lifetime_minutes is
CS_CONN_MAX_LIFETIME_MINUTES. The environment wins wherever both are set.
In the TOML file, numbers go in unquoted. A quoted number is ignored and you silently get the default instead.
Database
Section titled “Database”| Key | Environment | Default | |
|---|---|---|---|
db_host | CS_DB_HOST | required | |
db_name | CS_DB_NAME | required | |
db_user | CS_DB_USER | required | |
db_password | CS_DB_PASSWORD | ||
db_port | CS_DB_PORT | 5432 | |
db_sslmode | CS_DB_SSLMODE | disable | require, verify-ca or verify-full. Managed databases such as RDS and Cloud SQL usually need at least require. |
The three marked required have no default, and the server refuses to start without them rather than quietly connecting to something you did not mean.
Listening
Section titled “Listening”| Key | Environment | Default | |
|---|---|---|---|
host | CS_HOST | 0.0.0.0 | the bind address. Bare HOST is honoured too, since that is what most platforms inject. |
port | CS_PORT | 24275 | bare PORT likewise. |
public_base_url | CS_PUBLIC_BASE_URL | the address the SDK should send logs to, shown in the setup snippet. Empty means “use whatever address you reached the dashboard on”. |
public_base_url is deliberately not defaulted to host and port together. host is a bind
address, so that would produce http://0.0.0.0:24275, which no phone can reach. Set it when the
dashboard and the SDK reach this instance by different names, which is the usual case behind a
reverse proxy or on an internal hostname.
Connection pool
Section titled “Connection pool”| Key | Environment | Default | |
|---|---|---|---|
max_open_conns | CS_MAX_OPEN_CONNS | 25 | keep this under your database’s own connection limit |
max_idle_conns | CS_MAX_IDLE_CONNS | 5 | |
conn_max_lifetime_minutes | CS_CONN_MAX_LIFETIME_MINUTES | 30 |
Logging
Section titled “Logging”| Key | Environment | Default | |
|---|---|---|---|
log_level | CS_LOG_LEVEL | info | debug, info, warn or error |
log_format | CS_LOG_FORMAT | text or json. Empty picks by destination: text on a console, JSON in a file. | |
log_file | CS_LOG_FILE | empty means stdout | |
log_max_size_mb | CS_LOG_MAX_SIZE_MB | 100 | rotate once the file reaches this |
log_max_backups | CS_LOG_MAX_BACKUPS | 7 | how many rotated files to keep |
log_max_age_days | CS_LOG_MAX_AGE_DAYS | 30 | |
log_compress | CS_LOG_COMPRESS | true | gzip the rotated files |
Logs further down covers what these do in practice, and why info rather than debug.
There is no TLS setting. The server speaks plain HTTP and expects a reverse proxy in front of it to terminate TLS, which is covered further down.
Instance settings
Section titled “Instance settings”Everything else is edited in the dashboard under Settings and stored in Postgres:
| Setting | Effect |
|---|---|
| Timezone | Every timestamp the dashboard renders. Logs stay stored in UTC. |
| Clock | Whether times read as 15:04 or 3:04 PM. Moves every timestamp at once, same as the timezone. |
| Retention | How long logs are kept. Applied at the next nightly run. |
| Upload cap | Largest single upload. Applied to the very next upload. |
| Daily log cap | Per project, per day. 0 means uncapped. |
Database credentials deliberately stay out of the UI. A setting that can lock you out of the instance does not belong in the instance.
The database
Section titled “The database”The schema is migrated on startup, so there is nothing to create by hand. Eleven tables:
| Table | Holds |
|---|---|
projects | Name, description, session sampling rate |
project_secrets | SDK credentials |
project_members | Who can see which project, and at what level |
project_favorites | Per-user starred projects |
users | Email, name, bcrypt password hash, role |
user_sessions | Dashboard login sessions, 30 days |
personal_access_tokens | API tokens, stored as a SHA-256 hash and never as the token |
instance_settings | The settings above |
project_usage_daily | Per project, per day log counts, for the cap |
sessions | One row per app launch |
logs | The logs themselves |
Creating a project
Section titled “Creating a project”Sign in and use Add project. The wizard walks through the name, who can see it, and then shows the project id and secret once, with the snippet to paste into your app.
The credentials can be retrieved later from Project settings → SDK setup, and rotated there if a build leaks one.
Accounts
Section titled “Accounts”The first account created on a fresh instance is the super admin. Everyone after that is created
from Settings → Members, with a temporary password shown once to hand over. No email is
ever sent, which is what keeps docker compose up self-contained.
Email is the login identifier. There is no email-based password reset; a higher role resets it. If you lose the last super admin password, the binary can reset it directly:
./bin/code_scout reset-password --email=you@yourteam.devSee Access and Roles for what each role can do.
| Port | Service |
|---|---|
| 24275 | The server |
| 9089 | Templ proxy, development only |
Behind a reverse proxy
Section titled “Behind a reverse proxy”Most people put CodeScout behind nginx or Caddy for TLS. Two things about it are not ordinary HTTP, and a default proxy config breaks both without any error to tell you so:
- A WebSocket. The device end of a live session upgrades the connection. A proxy that does not forward the upgrade delivers an ordinary request instead, and no device can pair.
- Server-Sent Events. The dashboard watches a live session, and the log viewer tails, over SSE. A proxy that buffers responses holds those events back, so the page looks frozen while the server streams perfectly well.
Neither shows up in a log. Everything else on the instance keeps working, which makes this an easy half hour to lose.
# In the http block, or its own file in conf.d/map $http_upgrade $connection_upgrade { default upgrade; '' close;}
server { server_name codescout.example.com;
location / { proxy_pass http://127.0.0.1:24275; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# Live sessions upgrade to a WebSocket here. proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;
# SSE. Without this the dashboard's live tail delivers nothing until a # buffer happens to fill. proxy_buffering off;
# A live session can sit quiet for a long time. The 60 second default # closes an idle stream; the browser reconnects and no events are lost, # but it is needless churn. proxy_read_timeout 3600s; proxy_send_timeout 3600s; }}Use the map rather than hardcoding proxy_set_header Connection "upgrade". The hardcoded version
sends that header on every request, including ordinary page loads, and breaks HTTP keepalive to the
server. The map sends it only when a client actually asked to upgrade.
Caddy handles both by itself. reverse_proxy 127.0.0.1:24275 is the whole configuration:
WebSocket upgrades pass through, and responses are streamed rather than buffered.
Checking it worked
Section titled “Checking it worked”The proxy answers 401 for /api/live/socket whether or not upgrades pass, because credentials
are checked first, so curl cannot tell you. The real test is to pair a device: mint a code on the
Live screen, type it into the SDK’s floating button, and see the device appear as connected. If
it never does, the upgrade is not reaching the server.
The MCP endpoint needs nothing special
Section titled “The MCP endpoint needs nothing special”/api/mcp, which is how an AI agent reads your logs, is ordinary HTTP POSTs on the same port as
everything else. It holds no connection open and needs no proxy configuration beyond what the
dashboard already has, so if your pages load, it works. See
Reading CodeScout with an AI agent.
Health
Section titled “Health”GET /healthz reports the version, the commit and the state of the database. It sits outside the
request logging middleware so orchestrator probes do not bury real traffic, and it needs no login,
so it is the quickest way to ask a running instance what it is.
{"status":"ok","db":"ok","version":"1.1.0","commit":"1fb1e99"}By default the server writes to stdout, which is where both systemd and Docker expect it and which both already capture and rotate:
journalctl -u code_scout -f # a servicedocker compose logs -f code_scout # a containerTo write a file instead, set log_file. The server rotates it itself, so do not also point
logrotate at it or the two will fight over the same file.
log_level = "info" # debug, info, warn, errorlog_file = "/var/log/code_scout/code_scout.log" # empty means stdoutlog_max_size_mb = 100 # rotate when it reaches thislog_max_backups = 7 # keep this many rotated fileslog_max_age_days = 30log_compress = trueEvery key has a CS_ environment variable, so CS_LOG_LEVEL=debug works without editing the file.
make deploy-setup creates /var/log/code_scout and sets log_file for you.
Lines are human readable on a console and JSON in a file, since one is read by a person and the
other by a program. log_format forces either.
Errors and fatals always go to stderr as well, even when a file is configured. A server that fails to start writes the reason where you would look for it rather than only into a file.
log_level = "info" is deliberate. At debug every database call is logged, which is useful
when you are chasing something and overwhelming when you are not. Requests to /healthz and
/static/ are debug regardless, so a health probe every 15 seconds does not bury real traffic.
One line per request, when it finishes:
level=info msg=Request method=GET path=/project/x/logs status=200 duration_ms=14.207 bytes=4829 remote_ip=203.0.113.7 request_id=req-8f2arequest_id is on every line that request produced, including the database ones at debug, so
grep req-8f2a gives you the whole story of one request.
Behind a proxy, remote_ip is read from X-Forwarded-For. That header can be set by anyone talking
to the server directly, so it is for reading logs and never for a decision about access.
Which version am I running
Section titled “Which version am I running”Three places say so, and they all read the same constant:
- The foot of the sidebar on any project screen, and the top bar elsewhere. Hover it for the commit and the build time.
- Settings, General, About, which also has the update check.
GET /healthz, as above.
Quote the version and the commit when you report a problem. The version alone does not identify a build made between releases.
Updates
Section titled “Updates”CodeScout asks GitHub once a day whether a newer release exists, and shows a small badge next to the version when there is one. Only super admins see the badge, since nobody else can act on it.
The request carries nothing about your instance. No project names, no counts, no identifier of any kind. GitHub sees that an address asked a public question, which is less than it learns from someone opening the repository in a browser.
Turn it off in Settings, General, About. With it off the instance makes no outbound requests at all. If it cannot reach GitHub it says so on that card and is otherwise quiet, so an air-gapped instance is not nagged about a failure it cannot fix.
Upgrading
Section titled “Upgrading”Read the changelog first, then:
# Dockerdocker compose pull && docker compose up -d
# From sourcegit pull && make build && sudo systemctl restart code_scoutThere is no migration step. The schema is applied at startup, so a new binary brings the database up to date on its way up.
latest is the newest release, and 1.1.0 and 1.1 pin that release exactly. edge is built
from every push to main, which is the right choice for trying an unreleased fix and the wrong
one for anything you rely on. Every release also appears on the
releases page with its notes and a Linux
binary.
Pin a version. latest moving under a container that restarts on its own is how an upgrade
happens at a time nobody chose.
Next steps
Section titled “Next steps”- Getting Started: connect an app
- Access and Roles: who can see what
- Server REST API: the endpoints