opcgw is configured from the web UI (v2.5.0). You provide a bootstrap
config.toml(or the matchingOPCGW_*environment variables) for the first boot; opcgw reads it once into its SQLite database and from then on you manage everything — ChirpStack connection, OPC UA settings, and device/metric mappings — from the browser. Editingconfig.tomlafter the first boot has no effect.
Installation
Prerequisites
- Rust 1.95.0+ (Install)
- Docker & Docker Compose (optional, for containerized deployment)
- Running ChirpStack 4 instance with accessible gRPC API
- OPC UA client software (Ignition, KEPServerEx, UA Expert, etc.) for testing
From Source
# Clone repository
git clone https://github.com/guycorbaz/opcgw.git
cd opcgw
# Build
cargo build --release
# Binary location
./target/release/opcgw
Via Docker
# Build image
docker build -t opcgw:latest .
# Or use the published image (Docker Hub primary, GHCR mirror)
docker pull docker.io/gcorbaz/opcgw:2.3
Configuration
opcgw is zero-touch on first boot (v2.5.0): you do not edit a config file to
get started. Start the container (or binary) with the shipped placeholder
config.toml, open the web UI, and the setup wizard captures everything it needs.
1. First boot — the setup wizard (recommended)
- Start the gateway (see Running the Gateway below). With a pristine config, opcgw detects that ChirpStack credentials are missing and boots straight into first-run mode instead of aborting.
- Open the web UI in a browser:
http://<host>:8080/. You are redirected to the/setupwizard. - In the wizard, enter:
- ChirpStack server address (
http://your-chirpstack-server:8080) - ChirpStack tenant ID
- ChirpStack API token
- OPC UA password (for the OPC UA / web-UI login)
- ChirpStack server address (
- Submit. The wizard writes the secrets to
config/secrets.toml(chmod0600) and the rest of the configuration to opcgw’s SQLite database, then performs an in-process soft restart. The container itself is never restarted. - After the wizard, add your ChirpStack applications, devices and metrics from
the web UI’s ChirpStack inventory pickers — no hand-written
[[application]]blocks required.
No text-file editing is needed for a first boot. The shipped
config.tomlships withREPLACE_ME_WITH_*placeholders that the gateway recognises, so it starts, serves the wizard, and waits for you in the browser.
2. Optional — seed configuration from a TOML file (advanced)
config.toml is a bootstrap seed: opcgw reads it once on first start to
populate its SQLite database, then ignores it. Editing it after the first boot
has no effect — use the web UI. If you prefer to pre-seed instead of using the
wizard, copy the example and fill in the non-secret fields:
cp config/config.example.toml config/config.toml
# Global settings
[global]
debug = true # Set to false in production
# ChirpStack connection
[chirpstack]
server_address = "http://your-chirpstack-server:8080"
# NEVER put a real token inline. Keep the placeholder and inject the real value
# via the wizard, config/secrets.toml, or the OPCGW_CHIRPSTACK__API_TOKEN env var.
api_token = "REPLACE_ME_WITH_OPCGW_CHIRPSTACK__API_TOKEN_ENV_VAR"
tenant_id = "your-tenant-id" # From ChirpStack
polling_frequency = 10 # Poll every 10 seconds
retry = 3 # Retry 3 times
delay = 100 # 100ms between retries
# OPC UA server
[opcua]
application_name = "My IoT Gateway"
application_uri = "urn:my-company:opcua:gateway"
host_ip_address = "0.0.0.0" # Listen on all interfaces
host_port = 4840 # Standard OPC UA port
user_name = "admin"
# NEVER put a real password inline. Keep the placeholder and inject the real value
# via the wizard, config/secrets.toml, or the OPCGW_OPCUA__USER_PASSWORD env var.
user_password = "REPLACE_ME_WITH_OPCGW_OPCUA__USER_PASSWORD_ENV_VAR"
pki_dir = "./pki" # Certificate storage
create_sample_keypair = true # Auto-create certs
# Applications are optional in the seed — leave the list empty and populate it
# from the web UI's ChirpStack inventory pickers after first boot. A block like
# the one below pre-seeds one application with one device and one metric:
[[application]]
application_name = "Farm Sensors" # Display name in OPC UA
application_id = "1" # ChirpStack app ID
[[application.device]]
device_name = "Field A Sensor"
device_id = "device001"
[[application.device.read_metric]]
metric_name = "Soil Moisture"
chirpstack_metric_name = "soil_moisture" # Field name in ChirpStack
metric_type = "Float"
metric_unit = "%"
Secrets are never stored inline in examples. The
REPLACE_ME_WITH_*placeholders are recognised by the gateway; provide the real values through the wizard (which writesconfig/secrets.toml, chmod0600) or theOPCGW_*environment variables.
3. Staged “Apply changes” model
After the first boot, configuration edits made in the web UI stage into SQLite
rather than taking effect immediately. While changes are pending,
GET /api/status reports pending_changes: true. You apply all staged edits
together with a single explicit Apply changes action in the UI
(POST /api/config/apply), which triggers an in-process soft restart of the
data plane. The container is never restarted — there is no “restart to pick
up changes” step.
4. Validation
The gateway validates configuration on startup:
- server_address is valid URL format
- polling_frequency > 0
- No duplicate device IDs
- Secrets are not left as
REPLACE_ME_WITH_*placeholders (once configured)
A pristine install with no applications and missing ChirpStack credentials is valid — it boots into the setup wizard rather than failing. An empty application list is expected before you have run the wizard / inventory discovery. If validation fails, the error message shows which field is invalid.
Running the Gateway
From Source
# Default config location (./config/config.toml)
cargo run --release
# Custom config location
cargo run --release -- -c /etc/opcgw/config.toml
# With debug logging
cargo run --release -- -c config/config.toml -d
Via Docker
# Using docker-compose (recommended)
docker-compose up
# Logs will show:
# [INFO] Gateway started successfully
# [INFO] Poll interval: 10s
# [INFO] Applications: 2, Devices: 3
# [INFO] OPC UA endpoint: 0.0.0.0:4840
Output
Successful startup looks like:
2026-04-19T12:34:56.789Z INFO opcgw: starting opcgw
2026-04-19T12:34:56.850Z INFO opcgw: Gateway started successfully
poll_interval_seconds=10
application_count=2
device_count=3
opc_ua_endpoint=0.0.0.0:4840
chirpstack_server=http://localhost:8080
If startup fails, check:
- ChirpStack server is reachable
- API token is correct
- Tenant ID exists in ChirpStack
- No other service on port 4840
- Configuration file is valid TOML
Testing the Gateway
1. Verify ChirpStack Connection
Check logs for successful polling:
2026-04-19T12:35:06.789Z INFO opcgw::chirpstack: polled 3 devices
2. Connect OPC UA Client
Using UA Expert (free download):
- Launch UA Expert
- Double-click “Add Server”
- Enter address:
opc.tcp://localhost:4840 - Click “OK”
- Browse tree:
Server └── Devices ├── Farm Sensors (Application) │ ├── Field A Sensor (Device) │ │ ├── Soil Moisture (85.3) │ │ └── Temperature (22.5) │ └── Field B Sensor │ └── Soil Moisture (78.2) └── Building Management └── Floor 1 HVAC ├── Temperature Setpoint (21.0) └── Fan Running (true)
Using Ignition:
- Create OPC UA connection in Gateway
- Address:
opc.tcp://localhost:4840 - Username:
admin - Password: (from config)
- Browse tags and drag to windows
3. Check Logs
# Watch all gateway activity (one log file; rotates daily)
tail -f log/opcgw.log.*
# Filter to one subsystem
tail -f log/opcgw.log.* | grep 'opcgw::chirpstack' # ChirpStack polling
tail -f log/opcgw.log.* | grep 'opcgw::opc_ua' # OPC UA activity
# For deep per-module detail, raise the level (then recreate/restart):
# OPCGW_LOG_LEVEL=debug
4. Simulate Data Changes
In ChirpStack UI:
- Navigate to device
- Change a metric value
- Within 10 seconds, OPC UA client should show new value
- Check logs for “metric updated” events
Environment Variables
Override config values via environment variables. This is the recommended way to inject secrets (the ChirpStack API token and OPC UA password):
export OPCGW_CHIRPSTACK__SERVER_ADDRESS="http://prod-chirpstack:8080"
export OPCGW_CHIRPSTACK__API_TOKEN="secret-token"
export OPCGW_OPCUA__HOST_PORT="4860"
cargo run --release
Naming Convention: OPCGW_<SECTION>__<FIELD> — note the double underscore
(__) between the section and the field.
- Section and field are joined by
__(two underscores); nested keys use__too - Uppercase all letters
- Examples:
opcua.host_port→OPCGW_OPCUA__HOST_PORT;chirpstack.api_token→OPCGW_CHIRPSTACK__API_TOKEN;web.enabled→OPCGW_WEB__ENABLED
Environment variables take precedence over SQLite and config.toml.
Docker Deployment
docker-compose.yml
The repository ships a canonical docker-compose.yml; the essentials:
services:
opcgw:
image: docker.io/gcorbaz/opcgw:2.3
container_name: opcgw
restart: always
ports:
- "4840:4840" # OPC UA
- "8080:8080" # Web UI (setup wizard + configuration)
volumes:
- ./config:/usr/local/bin/config
- ./pki:/usr/local/bin/pki
- ./log:/usr/local/bin/log
- ./data:/usr/local/bin/data # SQLite DB — REQUIRED so metrics + config persist
environment:
# Secrets injected from .env (double underscore between section and field):
OPCGW_CHIRPSTACK__API_TOKEN: "${OPCGW_CHIRPSTACK__API_TOKEN}"
OPCGW_OPCUA__USER_PASSWORD: "${OPCGW_OPCUA__USER_PASSWORD}"
OPCGW_WEB__ENABLED: "true"
Bind-mounted directories must be owned by UID 10001 before first start (sudo chown -R 10001:10001 ./config ./pki ./log ./data). Without the ./data mount the SQLite database lives in the ephemeral container layer and is lost on docker compose down.
Run
docker-compose up -d
# View logs
docker-compose logs -f opcgw
# Stop
docker-compose down
Troubleshooting
“Failed to connect to ChirpStack”
error: connection refused to http://localhost:8080
Check:
- ChirpStack server is running:
telnet localhost 8080 - Correct IP/port in config
- Network connectivity (firewall, docker network)
“Configuration validation failed”
error: Configuration validation failed:
- chirpstack.polling_frequency: must be greater than 0
Fix: Check config file syntax, required fields, value ranges. See Configuration.
Note: an empty application list is not an error on a fresh install — opcgw boots into the setup wizard and you add applications/devices from the web UI’s ChirpStack inventory pickers. You only see validation failures for malformed values (bad URL, zero polling interval, duplicate device IDs, etc.).
“OPC UA port in use”
error: Failed to bind OPC UA server to 0.0.0.0:4840
Fix: Change port in config or kill process using 4840:
lsof -i :4840
kill <PID>
“Metrics not updating”
Check:
- Is polling working? Look for “polled X devices” in logs
- Is ChirpStack returning data? Check via
grpcurlor ChirpStack UI - Are metric names correct? Compare with ChirpStack device data
- Are you connecting to right OPC UA server?
“OPC UA client can’t connect”
Connection refused or timeout
Check:
- Gateway is running:
docker psorps aux | grep opcgw - Port is exposed:
netstat -tlnp | grep 4840 - Firewall allows port: check local firewall and network ACLs
- Client using correct address:
opc.tcp://host:4840
Next Steps
- Configure for your specific ChirpStack instance
- Integrate with your SCADA/MES system
- Review Architecture for deeper understanding
- Check Features for use case inspiration
- Monitor logs for operational insights
Questions? Open an issue on GitHub.