Update docs for Docker-on-deimos deployment, add grpc_plain_addr option

- ARCHITECTURE.md: document nginx + direct gRPC topology, add
  grpc_plain_addr config, update cert filenames to Let's Encrypt
  convention, add passwd to CLI table
- RUNBOOK.md: replace systemctl/journalctl with docker commands,
  fix cert path references, improve sync troubleshooting steps
- Example config: update cert paths, document grpc_plain_addr option
- grpcserver: add optional plaintext gRPC listener for reverse proxy
- config: add GRPCPlainAddr field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 08:58:01 -07:00
parent 2185bbe563
commit 691301dade
6 changed files with 80 additions and 41 deletions

View File

@@ -226,13 +226,14 @@ Built with Go `html/template` + htmx. Embedded via `//go:embed`.
```toml ```toml
[server] [server]
listen_addr = ":8443" listen_addr = ":8443" # REST API (HTTPS)
grpc_addr = ":9443" grpc_addr = ":9443" # gRPC (TLS, exposed directly)
tls_cert = "/srv/eng-pad-server/certs/cert.pem" grpc_plain_addr = "" # Optional plaintext gRPC for reverse proxy
tls_key = "/srv/eng-pad-server/certs/key.pem" tls_cert = "/srv/eng-pad-server/certs/fullchain.pem"
tls_key = "/srv/eng-pad-server/certs/privkey.pem"
[web] [web]
listen_addr = ":8080" listen_addr = ":8080" # Web UI (plain HTTP behind nginx)
base_url = "https://pad.metacircular.net" base_url = "https://pad.metacircular.net"
[database] [database]
@@ -255,13 +256,32 @@ level = "info"
## 9. Deployment ## 9. Deployment
### Production (deimos.wntrmute.net)
Docker container behind nginx on deimos:
- **Web UI**: `https://pad.metacircular.net` — nginx (port 443) → container:8080
- **gRPC sync**: `pad.metacircular.net:9443` — direct TLS, exposed via ufw
- **REST API**: container:8443 — not exposed externally
- **TLS**: Let's Encrypt cert for `pad.metacircular.net`, shared by nginx
and the container (copied to `/srv/eng-pad-server/certs/`)
```
Internet
├── :443 → nginx (TLS termination) → container:8080 (Web UI, plain HTTP)
└── :9443 → container:9443 (gRPC, direct TLS)
```
### Container ### Container
Multi-stage Docker build: Multi-stage Docker build:
1. Builder: `golang:1.25-alpine`, `CGO_ENABLED=0`, stripped binary 1. Builder: `golang:1.25-alpine`, `CGO_ENABLED=0`, stripped binary
2. Runtime: `alpine:latest`, non-root user 2. Runtime: `alpine:3.21`, non-root user (`engpad`, UID 1000)
### systemd ### systemd (alternative)
systemd units are provided for non-Docker deployments:
| Unit | Purpose | | Unit | Purpose |
|------|---------| |------|---------|
@@ -279,8 +299,8 @@ ReadWritePaths=/srv/eng-pad-server.
├── eng-pad-server.toml ├── eng-pad-server.toml
├── eng-pad-server.db ├── eng-pad-server.db
├── certs/ ├── certs/
│ ├── cert.pem │ ├── fullchain.pem # Let's Encrypt cert chain
│ └── key.pem │ └── privkey.pem # Let's Encrypt private key
└── backups/ └── backups/
``` ```
@@ -301,6 +321,7 @@ ReadWritePaths=/srv/eng-pad-server.
|---------|---------| |---------|---------|
| server | Start the service | | server | Start the service |
| init | Create database, first user | | init | Create database, first user |
| passwd | Reset a user's password |
| snapshot | Database backup (VACUUM INTO) | | snapshot | Database backup (VACUUM INTO) |
| status | Health check | | status | Health check |

View File

@@ -102,8 +102,8 @@ docker restart eng-pad-server
## 4. Alerting ## 4. Alerting
No automated alerting is configured. Monitor via: No automated alerting is configured. Monitor via:
- `systemctl status eng-pad-server` — process health - `docker ps | grep eng-pad-server` — container health
- `journalctl -u eng-pad-server --since "1 hour ago" | grep ERROR` — errors - `docker logs eng-pad-server --since 1h 2>&1 | grep ERROR` — errors
- Backup age: `ls -lt /srv/eng-pad-server/backups/ | head` - Backup age: `ls -lt /srv/eng-pad-server/backups/ | head`
## 5. Incident Procedures ## 5. Incident Procedures
@@ -122,9 +122,9 @@ No automated alerting is configured. Monitor via:
### Database Corruption ### Database Corruption
1. Stop the service: 1. Stop the container:
``` ```
systemctl stop eng-pad-server docker stop eng-pad-server
``` ```
2. Check integrity: 2. Check integrity:
``` ```
@@ -133,21 +133,20 @@ No automated alerting is configured. Monitor via:
3. If corrupted, restore from backup: 3. If corrupted, restore from backup:
``` ```
cp /srv/eng-pad-server/backups/eng-pad-server-LATEST.db /srv/eng-pad-server/eng-pad-server.db cp /srv/eng-pad-server/backups/eng-pad-server-LATEST.db /srv/eng-pad-server/eng-pad-server.db
chown engpad:engpad /srv/eng-pad-server/eng-pad-server.db
``` ```
4. Restart: 4. Restart:
``` ```
systemctl start eng-pad-server docker start eng-pad-server
``` ```
### Certificate Expiry ### Certificate Expiry
1. Check expiry: 1. Check expiry:
``` ```
openssl x509 -in /srv/eng-pad-server/certs/cert.pem -noout -dates openssl x509 -in /srv/eng-pad-server/certs/fullchain.pem -noout -dates
``` ```
2. Regenerate or renew the certificate. 2. Renew via certbot (see "Renew TLS Certificates" above).
3. Restart the service (picks up new certs on start). 3. Restart the container (picks up new certs on start).
### Disk Full ### Disk Full
@@ -167,11 +166,12 @@ No automated alerting is configured. Monitor via:
### Sync Fails from Android App ### Sync Fails from Android App
1. Verify server is reachable from the device's network. 1. Verify the app has the correct server URL (`pad.metacircular.net:9443`).
2. Check gRPC port is open: `ss -tlnp | grep 9443` 2. Use "Test Connection" in the app's sync settings for a specific error.
3. Check TLS cert is valid and trusted by the device. 3. Check gRPC port is open: `ss -tlnp | grep 9443`
4. Check credentials: verify the user exists via `eng-pad-server status`. 4. Check firewall: `sudo ufw status | grep 9443` (must be ALLOW).
5. Check server logs for auth failures: `journalctl -u eng-pad-server | grep UNAUTHENTICATED` 5. Check TLS cert is valid: `openssl x509 -in /srv/eng-pad-server/certs/fullchain.pem -noout -dates`
6. Check server logs for auth failures: `docker logs eng-pad-server 2>&1 | grep -i error`
## 6. Escalation ## 6. Escalation

View File

@@ -51,6 +51,7 @@ func runServer(cmd *cobra.Command, args []string) error {
// Start gRPC server // Start gRPC server
grpcSrv, err := grpcserver.Start(grpcserver.Config{ grpcSrv, err := grpcserver.Start(grpcserver.Config{
Addr: cfg.Server.GRPCAddr, Addr: cfg.Server.GRPCAddr,
PlainAddr: cfg.Server.GRPCPlainAddr,
TLSCert: cfg.Server.TLSCert, TLSCert: cfg.Server.TLSCert,
TLSKey: cfg.Server.TLSKey, TLSKey: cfg.Server.TLSKey,
DB: database, DB: database,

View File

@@ -1,8 +1,9 @@
[server] [server]
listen_addr = ":8443" listen_addr = ":8443"
grpc_addr = ":9443" grpc_addr = ":9443"
tls_cert = "/srv/eng-pad-server/certs/cert.pem" # grpc_plain_addr = "127.0.0.1:9444" # Optional: plaintext gRPC for reverse proxy
tls_key = "/srv/eng-pad-server/certs/key.pem" tls_cert = "/srv/eng-pad-server/certs/fullchain.pem"
tls_key = "/srv/eng-pad-server/certs/privkey.pem"
[web] [web]
listen_addr = ":8080" listen_addr = ":8080"

View File

@@ -20,6 +20,7 @@ type Config struct {
type ServerConfig struct { type ServerConfig struct {
ListenAddr string `toml:"listen_addr"` ListenAddr string `toml:"listen_addr"`
GRPCAddr string `toml:"grpc_addr"` GRPCAddr string `toml:"grpc_addr"`
GRPCPlainAddr string `toml:"grpc_plain_addr"`
TLSCert string `toml:"tls_cert"` TLSCert string `toml:"tls_cert"`
TLSKey string `toml:"tls_key"` TLSKey string `toml:"tls_key"`
} }

View File

@@ -14,6 +14,7 @@ import (
type Config struct { type Config struct {
Addr string Addr string
PlainAddr string
TLSCert string TLSCert string
TLSKey string TLSKey string
DB *sql.DB DB *sql.DB
@@ -50,5 +51,19 @@ func Start(cfg Config) (*grpc.Server, error) {
slog.Info("gRPC server started", "addr", cfg.Addr) slog.Info("gRPC server started", "addr", cfg.Addr)
go func() { _ = srv.Serve(lis) }() go func() { _ = srv.Serve(lis) }()
// Optional plaintext listener for reverse proxy (e.g. nginx grpc_pass).
if cfg.PlainAddr != "" {
plainLis, err := net.Listen("tcp", cfg.PlainAddr)
if err != nil {
return nil, fmt.Errorf("listen %s: %w", cfg.PlainAddr, err)
}
plainSrv := grpc.NewServer(
grpc.UnaryInterceptor(AuthInterceptor(cfg.DB)),
)
pb.RegisterEngPadSyncServiceServer(plainSrv, syncSvc)
slog.Info("gRPC plaintext server started", "addr", cfg.PlainAddr)
go func() { _ = plainSrv.Serve(plainLis) }()
}
return srv, nil return srv, nil
} }