Docker Compose on one host¶
Authority, Proxy and PostgreSQL as three containers on one Docker host, from
the release image. Connectors on other hosts. Profile: pilot.
| Placeholder | Meaning |
|---|---|
gate.example.com |
The name clients and connectors dial; resolves to the Docker host. |
registry.example.com/gdsgate:v0.10.0 |
The release image. See Running the image. |
https://idp.example.com/realms/gdsgate |
The OIDC issuer. |
Before you start¶
| Item | How to check | Reference |
|---|---|---|
| Docker Engine with Compose v2 | docker compose version |
|
| The image | docker image inspect registry.example.com/gdsgate:v0.10.0 |
Running the image |
Ports 50061–50063 free |
ss -ltn \| grep 5006 prints nothing |
Connector routes |
The same ports open inbound before you install. The administrator opens them; without them join and connect from other hosts fail |
from outside, after start: nc -vz gate.example.com 50061 |
Open the ports |
| The name resolves to the host | getent hosts gate.example.com |
Certificates |
A certificate pair for gate.example.com (Let's Encrypt or your own CA) |
openssl x509 -in fullchain.pem -noout -ext subjectAltName |
Certificates |
OIDC client gdsgate |
as on the single host | [oidc] |
production only: wrapped keyrings |
[security.integrity] |
For an IP-only entry, Authority can issue registration TLS from the cluster CA. Use these changes to the DNS commands below:
- Use
--domain 203.0.113.10and--join-relayon bothsetup prepareruns. - Omit
--enroll-cert,--enroll-key,--tls-certand--tls-key; skip public certificate installation. Keep the Authority state-volume mount shown below. - Publish
"50064:50064"underproxy.ports. Set Proxy'senroll.transport_sni=enroll.gdsgate.internalwithsetup change. - Record the printed cluster CA fingerprint. Connector joins through
https://203.0.113.10:50064with the approved pin and internal TLS name.
The TOML snapshots below show DNS mode. Keep the files generated for the selected mode. IP-only registration and trust.
Connector routes¶
| Route | Ports published | Here |
|---|---|---|
tunneled |
50061–50063 |
Used below. Connectors register and renew on 50061. Authority publishes nothing. |
tunneled, IP address only |
50061–50064 |
--join-relay on both setup prepare runs, and "50064:50064" under proxy.ports. |
fronted |
50061–50063 |
As tunneled. Each connector joins with --enroll-endpoint https://gate.example.com:50061 --route fronted. |
direct |
Authority's 50050, 50051 as well |
Not with this layout. |
Full reference: split deployment, routes.
Open the ports¶
Open the published ports before you install: 50061, 50062, 50063,
and 50064 for an IP address only. The host firewall, and a network
firewall in front of the host such as a cloud security group, are the
administrator's responsibility; compose up does not change them. While a
port is closed, gdsgate join on a connector host and gdsgate connect on
a workstation fail. What each port carries:
Endpoints.
| Firewall | Command |
|---|---|
| ufw | ufw allow 50061:50063/tcp |
| firewalld | firewall-cmd --permanent --add-port=50061-50063/tcp && firewall-cmd --reload |
nftables, the inet filter table of /etc/nftables.conf |
nft insert rule inet filter input tcp dport 50061-50063 accept, and the same rule in /etc/nftables.conf |
Configure¶
-
The directory, the store password and the certificate pair. The image runs as uid
10001; the files it reads belong to it:install -d -m 0755 /opt/gdsgate && cd /opt/gdsgate printf 'IMAGE=registry.example.com/gdsgate:v0.10.0\n' > .env install -d -m 0700 secrets openssl rand -hex 24 | tr -d '\n' > secrets/store_password openssl rand -hex 24 > secrets/postgres_password chmod 0600 secrets/postgres_password install -m 0600 -o 999 -g 999 secrets/store_password secrets/postgres_store_password chown 10001:10001 secrets/store_password chmod 0600 secrets/store_password curl -fsS https://gate.gds.dev/docs/deploy/files/cluster-ha/postgres-init.sh -o postgres-init.sh chmod 0755 postgres-init.sh install -d -m 0750 -o 10001 -g 10001 config config/tls install -m 0644 -o 10001 -g 10001 fullchain.pem config/tls/fullchain.pem install -m 0600 -o 10001 -g 10001 privkey.pem config/tls/privkey.pem install -m 0600 /dev/null proxy.envPostgreSQL initializes with its separate administrator password. The init script creates non-superuser role
gdsgatewith the application password. The store has no published host port; verify store isolation. -
/opt/gdsgate/compose.yaml:name: gdsgate x-gdsgate: &gdsgate image: ${IMAGE} # from .env; change it to upgrade read_only: true cap_drop: [ALL] security_opt: [no-new-privileges:true] tmpfs: [/tmp] restart: unless-stopped services: postgres: image: postgres:17 environment: POSTGRES_USER: postgres POSTGRES_DB: gdsgate POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password secrets: - postgres_password - {source: postgres_store_password, target: store_password, uid: "999", mode: 0400} volumes: - pgdata:/var/lib/postgresql/data - ./postgres-init.sh:/docker-entrypoint-initdb.d/10-gdsgate.sh:ro healthcheck: test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-U", "gdsgate", "-d", "gdsgate"] interval: 5s retries: 20 restart: unless-stopped authority: <<: *gdsgate command: ["--config", "/etc/gdsgate/authority.toml", "authority"] environment: GDSGATE_ADMIN_IDENTITY_DIR: /var/lib/gdsgate/admin # for the admin CLI in this container GDSGATE_ADMIN_ENDPOINT: https://authority:50051 secrets: [store_password] volumes: - ./config:/etc/gdsgate:ro - authority-state:/var/lib/gdsgate depends_on: postgres: { condition: service_healthy } proxy: <<: *gdsgate command: ["--config", "/etc/gdsgate/proxy.toml", "proxy"] env_file: [./proxy.env] # the one-time token, first start only volumes: - ./config:/etc/gdsgate:ro - proxy-state:/var/lib/gdsgate ports: - "50061:50061" # clients - "50062:50062" # connector tunnels - "50063:50063" # connector tunnels, WebSocket fallback depends_on: authority: { condition: service_healthy } secrets: postgres_store_password: file: ./secrets/postgres_store_password postgres_password: file: ./secrets/postgres_password store_password: file: ./secrets/store_password volumes: pgdata: authority-state: proxy-state:The image's health check requests
/readyzon127.0.0.1:9090, themetrics_listenboth files below set.depends_onwaits for it. -
Start PostgreSQL and write both configurations with the image. The first run joins the Compose network to reach the store:
docker compose up -d postgres docker run --rm --network gdsgate_default \ -v gdsgate_authority-state:/var/lib/gdsgate \ -v "$PWD/config:/etc/gdsgate" \ -v "$PWD/secrets/store_password:/run/secrets/store_password:ro" \ registry.example.com/gdsgate:v0.10.0 setup prepare \ --domain gate.example.com --security-profile pilot \ --role authority --authority-bind authority \ --store-url postgres://gdsgate@postgres:5432/gdsgate \ --store-password-file /run/secrets/store_password \ --enroll-cert /etc/gdsgate/tls/fullchain.pem --enroll-key /etc/gdsgate/tls/privkey.pem \ --oidc-issuer https://idp.example.com/realms/gdsgate --oidc-client-id gdsgate \ --unit-dir /tmp/units docker run --rm -v "$PWD/config:/etc/gdsgate" \ registry.example.com/gdsgate:v0.10.0 setup prepare \ --domain gate.example.com --security-profile pilot \ --role proxy --authority-host authority --state-dir /var/lib/gdsgate/proxy-state \ --enroll-cert /etc/gdsgate/tls/fullchain.pem \ --tls-cert /etc/gdsgate/tls/fullchain.pem --tls-key /etc/gdsgate/tls/privkey.pem \ --oidc-issuer https://idp.example.com/realms/gdsgate --oidc-client-id gdsgate \ --unit-dir /tmp/units docker run --rm -v "$PWD/config:/etc/gdsgate" \ registry.example.com/gdsgate:v0.10.0 setup change \ --set authority:audit.export_path=/var/lib/gdsgate/audit.jsonl--authority-bind authority: Authority binds the address its service name resolves to.--unit-dir /tmp/unitskeeps the systemd units inside the throwaway container. The Proxy dialsauthority, so its run writestransport_sni = "gate.example.com", the name on the certificate. -
The files as written, comments shortened.
config/authority.toml:store_url = "postgres://gdsgate@postgres:5432/gdsgate" # the postgres service store_password_file = "/run/secrets/store_password" # the Compose secret [security] profile = "pilot" [security.integrity] control_keyring = "/etc/gdsgate/control.keys" audit_keyring = "/etc/gdsgate/audit.keys" [endpoints] authority = "authority:50051" # bound inside the network authority_enroll = "authority:50050" proxy_public = "gate.example.com:50061" # told to joining nodes proxy_internal = "gate.example.com:50062" proxy_ws = "gate.example.com:50063" [telemetry] metrics_listen = "127.0.0.1:9090" # the health check's address [proxy] public_join = true # connectors renew at proxy_public [connector] authority_transport = "tunneled" [authority] enroll_cert_file = "/etc/gdsgate/tls/fullchain.pem" enroll_key_file = "/etc/gdsgate/tls/privkey.pem" [policy] path = "/etc/gdsgate/policy.cedar" [oidc] issuer = "https://idp.example.com/realms/gdsgate" client_id = "gdsgate" [audit] export_path = "/var/lib/gdsgate/audit.jsonl" # in the authority-state volumeconfig/proxy.toml. The three listeners bind every interface of the container (0.0.0.0:50061to0.0.0.0:50063); Compose publishes them:[endpoints] # proxy_public, proxy_internal, proxy_ws: 0.0.0.0:50061 to 0.0.0.0:50063 authority = "authority:50051" authority_enroll = "authority:50050" # where registration is forwarded [proxy] public_cert_file = "/etc/gdsgate/tls/fullchain.pem" public_key_file = "/etc/gdsgate/tls/privkey.pem" public_join = true # connectors register and renew on 50061 [enroll] endpoint = "https://authority:50050" state_dir = "/var/lib/gdsgate/proxy-state" # in the proxy-state volume transport_sni = "gate.example.com" [oidc] issuer = "https://idp.example.com/realms/gdsgate" client_id = "gdsgate" [security] profile = "pilot" [telemetry] metrics_listen = "127.0.0.1:9090" -
Check Authority's file:
docker run --rm -v "$PWD/config:/etc/gdsgate:ro" \ -v "$PWD/secrets/store_password:/run/secrets/store_password:ro" \ registry.example.com/gdsgate:v0.10.0 config check /etc/gdsgate/authority.toml --role authorityExpected:
0 error(s), 0 warning(s). The Proxy's file is checked once its token is inproxy.env(Start).
Start¶
docker compose up -d authority
docker compose exec -T authority gdsgate --config /etc/gdsgate/authority.toml \
authority bootstrap-admin --output-dir /var/lib/gdsgate/admin
printf 'GDSGATE_ENROLL_TOKEN=%s\n' "$(docker compose exec -T authority \
gdsgate --config /etc/gdsgate/authority.toml authority create-token --role proxy)" > proxy.env
docker run --rm --env-file proxy.env -v "$PWD/config:/etc/gdsgate:ro" \
registry.example.com/gdsgate:v0.10.0 config check /etc/gdsgate/proxy.toml --role proxy
docker compose up -d proxy
The Proxy check reads the token from its environment, as the proxy
service does. Expected: 0 error(s), 0 warning(s).
The administrative identity lasts one day; the same bootstrap-admin command
renews it. Administrative commands run as
docker compose exec -T authority gdsgate --config /etc/gdsgate/authority.toml authority ....
In a script, give each docker compose exec < /dev/null: it reads standard
input otherwise.
A connector on another host: the catalogue row and the token from
docker compose exec -T authority ..., then the tunneled tab of
VPS, a connector on another host.
Upgrade¶
sed -i 's|^IMAGE=.*|IMAGE=registry.example.com/gdsgate:v0.10.1|' .env
docker compose pull authority proxy
docker compose up -d
Authority and Proxy are recreated; the volumes stay. The Proxy starts with the
identity in proxy-state and does not use the token again. Read the
upgrade guide first.
Backup¶
| What | How |
|---|---|
| The store | docker compose exec -T postgres pg_dump -U gdsgate -Fc gdsgate > gdsgate.dump |
| The keyrings, the configuration, the certificate pair | /opt/gdsgate/config, kept apart from the store dump |
| Database administrator and application passwords | /opt/gdsgate/secrets; protected storage |
A store dump without the keyrings cannot be verified. See Keeping keyrings out of the database backup.
Verify¶
| Command | Expected | If not: cause → fix |
|---|---|---|
docker compose ps |
authority, postgres, proxy each Up ... (healthy) |
docker compose logs authority; see the rows below. |
docker compose logs authority |
auth service serving gRPC ... enroll_mode="TLS from the configured certificate" |
Permission denied (os error 13) → a file in config/ or secrets/ not owned by uid 10001. |
docker compose logs authority |
no readable beyond its owner warning |
a secret file this configuration names is readable beyond its owner → chmod 0600 secrets/store_password. |
docker compose logs proxy |
enrolled and persisted a new transport identity, then proxy serving gRPC (mTLS (enrolled), public TLS from the configured certificate) |
certificate refused (...); checked for the name ... → the certificate does not carry gate.example.com, or the name in transport_sni differs. unknown token → a new token into proxy.env, then docker compose up -d proxy. |
docker compose exec -T authority gdsgate --config /etc/gdsgate/authority.toml authority list-nodes |
admin, proxy rows, active |
|
On the connector host: gdsgate join gate.example.com ... --enroll-endpoint https://gate.example.com:50061 |
ok: joined gate.example.com as conn-1 · role connector |
See VPS verify. |
docker compose exec -T authority gdsgate --config /etc/gdsgate/authority.toml authority list-offered |
demo-ssh ssh ... yes |
|
Workstation: gdsgate connect gate.example.com |
ok: wrote ~/.gdsgate/config.toml |
See Certificates. |
After an upgrade: docker compose logs proxy |
reusing persisted transport identity |
enrolled and persisted a new transport identity → the proxy-state volume was lost; revoke the old Proxy row. |
After these checks pass, complete the access checks from a client workstation.
Profile¶
pilot, the same matrix rows as the single host.