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
[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 |

View File

@@ -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

View File

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

View File

@@ -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"
# 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"

View File

@@ -20,6 +20,7 @@ type Config struct {
type ServerConfig struct {
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"`
}

View File

@@ -14,6 +14,7 @@ import (
type Config struct {
Addr string
PlainAddr string
TLSCert string
TLSKey string
DB *sql.DB
@@ -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
}