Run a public API node¶
A public API node is a healthy standard Koinos node with API and index services enabled. Operate those services through the official Koinos Compose project.
The upstream api profile enables JSON-RPC, REST, gRPC, transaction store,
contract metadata store, and account history. It does not require
block_producer.
Public and private interfaces¶
Keep the Koinos API ports on loopback and expose only a hardened HTTPS reverse proxy:
| Protocol | Local host port | Public route | Verification |
|---|---|---|---|
| JSON-RPC | 127.0.0.1:8080 |
https://rpc.example.com/ |
HTTP POST |
| REST and Swagger | 127.0.0.1:3000 |
https://rpc.example.com/v1/... and /swagger |
HTTP GET |
| gRPC | 127.0.0.1:50051 |
grpc.example.com:443 |
descriptor-based gRPC |
RabbitMQ 5672 and its administration UI 15672 must remain private. P2P
8888 may be public when the host participates in peer-to-peer networking.
1. Enable the API profile¶
Start from the same official checkout and basedir as the standard node.
Preserve the existing .env, then edit these values:
| Setting | Value |
|---|---|
COMPOSE_PROFILES |
api |
JSONRPC_INTERFACE |
127.0.0.1 |
JSONRPC_PORT |
8080 |
REST_INTERFACE |
127.0.0.1 |
REST_PORT |
3000 |
GRPC_INTERFACE |
127.0.0.1 |
GRPC_PORT |
50051 |
AMQP_INTERFACE |
127.0.0.1 |
AMQP_ADMIN_INTERFACE |
127.0.0.1 |
Keep the image tags from the selected release or commit. Validate and start:
cd /opt/koinos
docker compose config
docker compose up -d
docker compose ps
Expect the five required services plus jsonrpc, rest, grpc,
transaction_store, contract_meta_store, and account_history. Expect no
block_producer container.
2. Verify each local protocol¶
Check JSON-RPC:
curl --fail http://127.0.0.1:8080/ \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"chain.get_head_info","params":{},"id":1}'
Check REST:
curl --fail http://127.0.0.1:3000/v1/chain/head_info
Check gRPC with grpcurl and the descriptor set from the same selected
release or commit:
grpcurl -plaintext \
-protoset /opt/koinos/config/koinos_descriptors.pb \
-d '{}' \
127.0.0.1:50051 \
koinos.rpc.chain.chain_rpc/get_head_info
Koinos gRPC does not advertise reflection, so a reflection-only test is not sufficient. Plaintext is appropriate only for this loopback check; public gRPC must use TLS.
3. Publish through a reverse proxy¶
This direct procedure uses the standard Ubuntu nginx package. It publishes:
- JSON-RPC and REST/Swagger as
https://rpc.example.com; - gRPC as
grpc.example.com:443; - no raw Koinos or RabbitMQ port.
Replace both example hostnames and the single allowed browser origin before validation.
Install nginx, Certbot, and the network test tools:
sudo apt-get update
sudo apt-get install -y nginx certbot netcat-openbsd ufw
sudo systemctl enable --now nginx
Point both DNS names at this host. Allow SSH and the reverse proxy through the host firewall; allow P2P only when this node accepts inbound peers:
read -r -p 'Administrative CIDR allowed to use SSH: ' admin_cidr
test -n "$admin_cidr"
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from "$admin_cidr" to any port 22 proto tcp
sudo ufw allow 'Nginx Full'
read -r -p 'Allow inbound P2P on port 8888? [yes/NO]: ' allow_p2p
if [[ "$allow_p2p" = yes ]]; then
sudo ufw allow 8888/tcp
fi
sudo ufw show added
Before enabling UFW, open a second SSH session from the configured
administrative CIDR and keep the first session open. In the second session,
verify that a new login and sudo command succeed. Then return to the first
session and enable the prepared rules deliberately:
read -r -p 'Type ENABLE after the second SSH session succeeds: ' confirmation
test "$confirmation" = ENABLE
sudo ufw --force enable
sudo ufw status verbose
sudo ss -lntp
Stop if the second SSH session cannot connect or use sudo. Remove any broader
pre-existing SSH rule only after UFW is active and the restricted recovery
path works. Ports 8080, 3000, 50051, 5672, and 15672 need no public
firewall rule. Section 4 completes the check from a different machine.
Obtain one certificate containing both DNS names through the initial Ubuntu nginx web root:
sudo certbot certonly --webroot --webroot-path /var/www/html \
-d rpc.example.com \
-d grpc.example.com
sudo certbot certificates
Create the HTTP-level rate-limit and CORS-origin definitions:
sudoedit /etc/nginx/conf.d/koinos-rpc-global.conf
map $http_origin $koinos_cors_origin {
default "";
"https://app.example.com" $http_origin;
}
limit_req_zone $binary_remote_addr zone=koinos_rpc:10m rate=10r/s;
Create the complete site:
sudoedit /etc/nginx/sites-available/koinos-rpc
server {
listen 80;
listen [::]:80;
server_name rpc.example.com grpc.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 308 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name rpc.example.com;
ssl_certificate
/etc/letsencrypt/live/rpc.example.com/fullchain.pem;
ssl_certificate_key
/etc/letsencrypt/live/rpc.example.com/privkey.pem;
server_tokens off;
client_max_body_size 1m;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
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 https;
add_header Access-Control-Allow-Origin
$koinos_cors_origin always;
add_header Access-Control-Allow-Methods
"GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers
"Content-Type, Authorization" always;
add_header Vary "Origin" always;
add_header X-Content-Type-Options "nosniff" always;
if ($request_method = OPTIONS) {
return 204;
}
location = / {
limit_req zone=koinos_rpc burst=20 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}
location ^~ /v1/ {
limit_req zone=koinos_rpc burst=20 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
location = /swagger {
limit_req zone=koinos_rpc burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location ^~ /swagger/ {
limit_req zone=koinos_rpc burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location ^~ /api/ {
limit_req zone=koinos_rpc burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location ^~ /_next/ {
limit_req zone=koinos_rpc burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location = /favicon.ico {
proxy_pass http://127.0.0.1:3000;
}
location / {
return 404;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name grpc.example.com;
ssl_certificate
/etc/letsencrypt/live/rpc.example.com/fullchain.pem;
ssl_certificate_key
/etc/letsencrypt/live/rpc.example.com/privkey.pem;
server_tokens off;
client_max_body_size 1m;
location / {
limit_req zone=koinos_rpc burst=20 nodelay;
limit_req_status 429;
grpc_connect_timeout 5s;
grpc_read_timeout 30s;
grpc_send_timeout 30s;
grpc_set_header Host $host;
grpc_pass grpc://127.0.0.1:50051;
}
}
The 10r/s rate, burst=20, 1m body limit, and timeout values are
conservative starting controls, not universal capacity recommendations.
Load-test the intended methods and payload sizes on a test server, monitor
queue and node saturation, and change them only from measured evidence.
Enable and validate the site before reloading:
test -L /etc/nginx/sites-enabled/koinos-rpc ||
sudo ln -s /etc/nginx/sites-available/koinos-rpc \
/etc/nginx/sites-enabled/koinos-rpc
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl --no-pager --full status nginx
sudo certbot renew --dry-run
The Koinos ports must remain bound to loopback even when a host firewall is also present. Defense in depth matters: a future firewall change must not publish the raw services automatically.
4. Verify exposure from two locations¶
On the node host, inspect listeners:
sudo ss -lntp
The API, RabbitMQ, and RabbitMQ administration ports should show
127.0.0.1, not 0.0.0.0 or [::].
From a different machine or network, list every relevant port:
for port in 22 80 443 8888 5672 15672 8080 3000 50051; do
if nc -z -w 3 rpc.example.com "$port"; then
printf 'OPEN %s\n' "$port"
else
printf 'CLOSED %s\n' "$port"
fi
done
Expect 80 and 443 to be open. Port 22 should be restricted to the
administration source, and 8888 may be open when inbound P2P is intentional.
Expect 5672, 15672, 8080, 3000, and 50051 to be closed externally.
Run every public protocol test from that external machine. Check JSON-RPC:
curl --fail --silent --show-error https://rpc.example.com/ \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"chain.get_head_info","params":{},"id":1}' |
jq -e '.result.head_topology.height | tonumber > 0'
Check REST and Swagger:
curl --fail --silent --show-error \
https://rpc.example.com/v1/chain/head_info |
jq -e '.head_topology.height | tonumber > 0'
curl --fail --head https://rpc.example.com/swagger
Check the allowed CORS preflight:
curl --fail --include --request OPTIONS https://rpc.example.com/ \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: Content-Type'
The response must be 204 and must return exactly the configured
Access-Control-Allow-Origin, not *.
Copy koinos_descriptors.pb from the same selected release or commit to the
external test machine, then check public gRPC over TLS:
grpcurl \
-protoset ./koinos_descriptors.pb \
-d '{}' \
grpc.example.com:443 \
koinos.rpc.chain.chain_rpc/get_head_info
Monitor latency, non-2xx responses, container restarts, queue pressure, head freshness, disk growth, certificate expiry, nginx log growth, and host saturation.
Continue with Security and Operations and recovery.