From 691301daded3a15a937397e9c5be95ab73175070 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 25 Mar 2026 08:58:01 -0700 Subject: [PATCH] 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) --- ARCHITECTURE.md | 39 ++++++++++++++++++++++------- RUNBOOK.md | 28 ++++++++++----------- cmd/eng-pad-server/server.go | 11 ++++---- deploy/examples/eng-pad-server.toml | 9 ++++--- internal/config/config.go | 9 ++++--- internal/grpcserver/server.go | 25 ++++++++++++++---- 6 files changed, 80 insertions(+), 41 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e44734f..5fa70b2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -226,13 +226,14 @@ Built with Go `html/template` + htmx. Embedded via `//go:embed`. ```toml [server] -listen_addr = ":8443" -grpc_addr = ":9443" -tls_cert = "/srv/eng-pad-server/certs/cert.pem" -tls_key = "/srv/eng-pad-server/certs/key.pem" +listen_addr = ":8443" # REST API (HTTPS) +grpc_addr = ":9443" # gRPC (TLS, exposed directly) +grpc_plain_addr = "" # Optional plaintext gRPC for reverse proxy +tls_cert = "/srv/eng-pad-server/certs/fullchain.pem" +tls_key = "/srv/eng-pad-server/certs/privkey.pem" [web] -listen_addr = ":8080" +listen_addr = ":8080" # Web UI (plain HTTP behind nginx) base_url = "https://pad.metacircular.net" [database] @@ -255,13 +256,32 @@ level = "info" ## 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 Multi-stage Docker build: 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 | |------|---------| @@ -279,8 +299,8 @@ ReadWritePaths=/srv/eng-pad-server. ├── eng-pad-server.toml ├── eng-pad-server.db ├── certs/ -│ ├── cert.pem -│ └── key.pem +│ ├── fullchain.pem # Let's Encrypt cert chain +│ └── privkey.pem # Let's Encrypt private key └── backups/ ``` @@ -301,6 +321,7 @@ ReadWritePaths=/srv/eng-pad-server. |---------|---------| | server | Start the service | | init | Create database, first user | +| passwd | Reset a user's password | | snapshot | Database backup (VACUUM INTO) | | status | Health check | diff --git a/RUNBOOK.md b/RUNBOOK.md index b8e271d..f293078 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -102,8 +102,8 @@ docker restart eng-pad-server ## 4. Alerting No automated alerting is configured. Monitor via: -- `systemctl status eng-pad-server` — process health -- `journalctl -u eng-pad-server --since "1 hour ago" | grep ERROR` — errors +- `docker ps | grep eng-pad-server` — container health +- `docker logs eng-pad-server --since 1h 2>&1 | grep ERROR` — errors - Backup age: `ls -lt /srv/eng-pad-server/backups/ | head` ## 5. Incident Procedures @@ -122,9 +122,9 @@ No automated alerting is configured. Monitor via: ### Database Corruption -1. Stop the service: +1. Stop the container: ``` - systemctl stop eng-pad-server + docker stop eng-pad-server ``` 2. Check integrity: ``` @@ -133,21 +133,20 @@ No automated alerting is configured. Monitor via: 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 - chown engpad:engpad /srv/eng-pad-server/eng-pad-server.db ``` 4. Restart: ``` - systemctl start eng-pad-server + docker start eng-pad-server ``` ### Certificate 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. -3. Restart the service (picks up new certs on start). +2. Renew via certbot (see "Renew TLS Certificates" above). +3. Restart the container (picks up new certs on start). ### Disk Full @@ -167,11 +166,12 @@ No automated alerting is configured. Monitor via: ### Sync Fails from Android App -1. Verify server is reachable from the device's network. -2. Check gRPC port is open: `ss -tlnp | grep 9443` -3. Check TLS cert is valid and trusted by the device. -4. Check credentials: verify the user exists via `eng-pad-server status`. -5. Check server logs for auth failures: `journalctl -u eng-pad-server | grep UNAUTHENTICATED` +1. Verify the app has the correct server URL (`pad.metacircular.net:9443`). +2. Use "Test Connection" in the app's sync settings for a specific error. +3. Check gRPC port is open: `ss -tlnp | grep 9443` +4. Check firewall: `sudo ufw status | grep 9443` (must be ALLOW). +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 diff --git a/cmd/eng-pad-server/server.go b/cmd/eng-pad-server/server.go index e3defcf..9a04eb5 100644 --- a/cmd/eng-pad-server/server.go +++ b/cmd/eng-pad-server/server.go @@ -50,11 +50,12 @@ func runServer(cmd *cobra.Command, args []string) error { // Start gRPC server grpcSrv, err := grpcserver.Start(grpcserver.Config{ - Addr: cfg.Server.GRPCAddr, - TLSCert: cfg.Server.TLSCert, - TLSKey: cfg.Server.TLSKey, - DB: database, - BaseURL: cfg.Web.BaseURL, + Addr: cfg.Server.GRPCAddr, + PlainAddr: cfg.Server.GRPCPlainAddr, + TLSCert: cfg.Server.TLSCert, + TLSKey: cfg.Server.TLSKey, + DB: database, + BaseURL: cfg.Web.BaseURL, }) if err != nil { return fmt.Errorf("start grpc: %w", err) diff --git a/deploy/examples/eng-pad-server.toml b/deploy/examples/eng-pad-server.toml index 0bada29..5feef8c 100644 --- a/deploy/examples/eng-pad-server.toml +++ b/deploy/examples/eng-pad-server.toml @@ -1,8 +1,9 @@ [server] -listen_addr = ":8443" -grpc_addr = ":9443" -tls_cert = "/srv/eng-pad-server/certs/cert.pem" -tls_key = "/srv/eng-pad-server/certs/key.pem" +listen_addr = ":8443" +grpc_addr = ":9443" +# grpc_plain_addr = "127.0.0.1:9444" # Optional: plaintext gRPC for reverse proxy +tls_cert = "/srv/eng-pad-server/certs/fullchain.pem" +tls_key = "/srv/eng-pad-server/certs/privkey.pem" [web] listen_addr = ":8080" diff --git a/internal/config/config.go b/internal/config/config.go index a973002..4e89690 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,10 +18,11 @@ type Config struct { } type ServerConfig struct { - ListenAddr string `toml:"listen_addr"` - GRPCAddr string `toml:"grpc_addr"` - TLSCert string `toml:"tls_cert"` - TLSKey string `toml:"tls_key"` + ListenAddr string `toml:"listen_addr"` + GRPCAddr string `toml:"grpc_addr"` + GRPCPlainAddr string `toml:"grpc_plain_addr"` + TLSCert string `toml:"tls_cert"` + TLSKey string `toml:"tls_key"` } type WebConfig struct { diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go index 36030e0..d38f6ce 100644 --- a/internal/grpcserver/server.go +++ b/internal/grpcserver/server.go @@ -13,11 +13,12 @@ import ( ) type Config struct { - Addr string - TLSCert string - TLSKey string - DB *sql.DB - BaseURL string + Addr string + PlainAddr string + TLSCert string + TLSKey string + DB *sql.DB + BaseURL string } // Start creates and starts the gRPC server. It returns the server so the @@ -50,5 +51,19 @@ func Start(cfg Config) (*grpc.Server, error) { slog.Info("gRPC server started", "addr", cfg.Addr) 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 }