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:
@@ -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 |
|
||||
|
||||
|
||||
28
RUNBOOK.md
28
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user