fintual-backend-devops-go / deploy
README.md

Native Linux operation

No service was installed on the development laptop. contentd.service is a deployment example for a dedicated host with systemd and an existing PostgreSQL server. The HTTP listener defaults to loopback. Put a TLS-terminating reverse proxy in front; restrict network access there. Authentication remains out of scope, so do not mistake operational hardening for permission to expose writes publicly.

Release procedure

  1. Build with bin/build on the target architecture. Copy go-service/contentd into a versioned directory under /opt/contentd/releases/. Keep the prior binary.
  2. Create an unprivileged OS account named contentd. Create a PostgreSQL database and a separate migration-owner role. Back up existing data before schema changes.
  3. Run the binary's explicit migrate command with the owner's DATABASE_URL. It uses a transaction, advisory lock, migration version and SHA-256 checksum. Repeating it is safe. It refuses existing untracked blog tables; this is not an automatic live-Django cutover procedure. Serving never runs migrations.
  4. Grant the runtime role CONNECT, schema USAGE, SELECT/INSERT/UPDATE on the blog tables, sequence USAGE/SELECT, and SELECT on content_schema_migrations. It needs no CREATE, DROP, DELETE, or superuser rights. Configure local peer authentication or a protected password file. For a remote database use verified TLS.
  5. Install the example unit and a root-managed /etc/contentd/environment, mode 0600, populated from environment.example. Point current to the new release. Adjust hosts and paths deliberately. Start the service and check /readyz.

The harness's assignment_harness_* databases and ownership marker are only for tests. Do not use the harness to initialize, seed, or benchmark a real database. The production migrator can initialize a normal database without that marker. Set DATABASE_URL explicitly for both migration and serving on a production host; when unset or empty, both commands use dbname=assignment_harness_native.

Profile differences

Setting Compatibility Production, default
Legacy framework errors Captured plaintext Python errors for observed cases Generic JSON 500, no traceback
Required configuration None; database defaults to assignment_harness_native Exact ALLOWED_HOSTS; same database default
Schema check at startup Existing compatible tables Version 1 and matching checksum required
Health endpoints Absent GET /healthz and /readyz
Request body limit Reference behavior 1 MiB, 413 on overflow including chunked input
Request admission Unbounded handler admission 64 active application handlers, excess gets 503
SQL connections 8 by default 8 by default, configurable 1–128
HTTP deadlines Reference comparison profile Headers 5s, read/query context 15s, write 30s, idle 60s
Header size Go default 32 KiB nominal limit plus Go parser allowance
Logs Startup/shutdown, optional SQL diagnostics JSON request ID, method, status and elapsed time
Proxy headers Unused Untrusted and ignored; Host checked directly

Both profiles preserve partial writes, draft visibility, the non-atomic counter, response shape and ordering rules. Production errors and operational limits are explicit differences, not a claim that the production profile passes unchanged debug-output golden files. Request deadlines can cancel database operations but do not undo already committed writes. The saved Python errors in compatibility mode are emulation assets, not genuine Go stack traces.

DB_MAX_CONNS and MAX_INFLIGHT have validated bounds. Invalid profiles, absent required settings, unreachable databases and incompatible schema fail startup. SQL_TRACE is rejected in production. Logs omit bodies, SQL bind values and query strings. Incoming request IDs are replaced rather than trusted.

Readiness, shutdown and rollback

Liveness checks the HTTP process. Readiness pings PostgreSQL with a one-second deadline; it is not a continuous schema-drift check or dependency-wide health audit. Use an allowed Host for probes. The app stops accepting connections on SIGTERM and waits up to ten seconds for handlers before forcing closure. systemd permits fifteen seconds. Black-box tests hold a row lock, send a real request and SIGTERM, release the lock, then verify a successful response and zero-exit shutdown.

For this first schema version, rollback means stop, repoint current at the prior compatible binary, start, and verify readiness. Future schema changes need explicit backward-compatibility and restore procedures. No automatic destructive down migration or database deletion is provided.

The unit limits privileges, writable paths, process count, descriptors and memory. Its 1 GiB cap is an example, not a measured production capacity recommendation. Unpaginated results can still consume substantial memory. Verify backup restoration, TLS/proxy settings, logging retention, capacity and monitoring on the actual host before exposing the application.