11 KiB
Debian sandbox deployment
Target: https://sandbox-guestops.futuresens.co.uk. These are reviewable deployment instructions; creating these files does not deploy or change the server.
1. Prepare the host
Check the existing Nginx, Docker, MongoDB and firewall configuration before installation. Preserve existing services. The provided Compose file creates a dedicated MongoDB instance for GuestOps with no host port; if MongoDB already exists on the host, use a reviewed private connection and application-scoped user instead of starting a conflicting instance.
Install Docker Engine/Compose and Nginx using their official Debian instructions. Keep SSH access unchanged. Only HTTPS/HTTP for this hostname need public access; port 8080 is loopback-only and MongoDB has no published port.
Install the versioned source package through the Futuresens Ansible repository. Do not clone GuestOps from the target server and do not place Gitea credentials on it. The playbook should extract the package into a dedicated application directory. Copy .env.example to .env, set permissions to 600, and fill two different MongoDB passwords generated with openssl rand -hex 32. Use hex values so they are safe in the MongoDB URI. Store real values only on the server or in its secret-management system.
The MongoDB initialization script runs only on a new volume. Changing .env later does not rotate existing database users. Rotate those credentials through MongoDB administration and update application configuration together.
2. Package and install with Ansible
GuestOps follows the CMS/CMSFront deployment pattern: Gitea stores the application source, a specific committed version is compressed, and a version-selected Ansible playbook installs it. No Gitea Actions runner is required.
On the trusted packaging machine, run the automated checks, select the full commit SHA, and create the archive from that committed tree rather than from a working directory:
git archive --format=tar.gz --prefix=GuestOps-0.2.0/ \
--output GuestOps-0.2.0.tar.gz FULL_40_CHARACTER_SHA
sha256sum GuestOps-0.2.0.tar.gz > GuestOps-0.2.0.tar.gz.sha256
Store the archive and checksum under a versioned GuestOps files directory in the private Ansible repository. The GuestOps playbook and environment variables should select that version, copy and verify the archive, extract it into the application directory, preserve the private .env and provider configuration, and build images tagged with the full source commit:
docker build --target api -t guestops-api:FULL_40_CHARACTER_SHA .
docker build --target worker -t guestops-worker:FULL_40_CHARACTER_SHA .
Record the resulting immutable image IDs and bind them to the source archive with deploy/release_record.py. Retain the source archive, checksum, release record, its checksum, build output, commit and Ansible run result in the restricted release store. Set GUESTOPS_API_IMAGE=guestops-api:FULL_40_CHARACTER_SHA and GUESTOPS_WORKER_IMAGE=guestops-worker:FULL_40_CHARACTER_SHA in .env. Then the playbook runs:
docker compose config --quiet
docker compose up -d --no-build
curl --fail http://127.0.0.1:8080/health
Do not run docker compose config without --quiet in shared logs: expanded configuration contains secrets. Do not store .env, provider credentials, host inventory secrets, or restricted evidence in either application repository. Ansible must stop on a checksum, commit, version, build, or health-check mismatch.
3. Provision the first hotel owner
The API has an administrative bootstrap command. It creates one hotel and one owner, refuses an existing email address, and does not reset passwords. Run it with environment variables passed from a private terminal; do not put the password directly in shell history or a repository file.
read -r -p 'Owner email: ' BOOTSTRAP_EMAIL
read -r -p 'Hotel name: ' BOOTSTRAP_HOTEL
read -r -s -p 'Owner password (14+ characters): ' BOOTSTRAP_PASSWORD
printf '\n'
export BOOTSTRAP_EMAIL BOOTSTRAP_HOTEL BOOTSTRAP_PASSWORD
docker compose run --rm --no-deps -e BOOTSTRAP_EMAIL -e BOOTSTRAP_HOTEL -e BOOTSTRAP_PASSWORD api --bootstrap
unset BOOTSTRAP_EMAIL BOOTSTRAP_HOTEL BOOTSTRAP_PASSWORD
There is no development/demo account in production. Owners can issue staff invitation and assisted recovery links through Team; see account setup. Passwords must be 14–128 characters. Public self-registration is not enabled.
4. Configure HTTPS
Verify the hostname's DNS resolves to this server. Obtain a valid certificate for it using your existing ACME/Certbot process. The Nginx example expects a Let's Encrypt certificate. For first issuance, configure the port-80 ACME location before enabling the port-443 block; do not point Nginx at missing certificate files.
Merge deploy/nginx.conf into the existing host configuration, check with nginx -t, then reload Nginx. Confirm HTTPS serves the login page. API cookies are always Secure outside preview mode; logging in through plain HTTP is intentionally unsupported.
Compose reserves the private bridge 172.30.87.0/24, with gateway 172.30.87.1. The API trusts the host gateway's X-Forwarded-Proto header so Nginx's HTTPS connections receive secure session and CSRF cookies. Check for an existing network using that range. If it conflicts, set both GUESTOPS_SUBNET and GUESTOPS_GATEWAY in .env to a free matching subnet and gateway. Do not replace this with unrestricted forwarded-header trust.
The login rate limiter uses the client address forwarded by the explicitly configured host proxy (10 attempts per client address per minute). ASP.NET accepts one forwarding hop only from Proxy__KnownAddress; arbitrary client-supplied forwarding headers are not trusted. Keep the API port loopback-only and update the known address together with any reviewed Compose subnet change.
5. Configure Google
Create or select your Google Cloud project, enable Gmail API, and configure a Web application OAuth client. Register this exact redirect URI:
https://sandbox-guestops.futuresens.co.uk/api/integrations/google/callback
Configure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in the server's .env and recreate the API/worker services. Sign in as hotel owner, open Settings and connect a dedicated test mailbox. By default the only requested Gmail scope is gmail.readonly. Optional staff-approved sending adds gmail.send after server configuration and reconnection; see AI drafts and reply delivery.
OAuth requests expire after ten minutes, are bound to the signed-in user and hotel, and can be consumed once. Refresh tokens are protected with ASP.NET Data Protection. API and worker share the private persistent key volume; back it up securely with the database. Losing it prevents existing mailbox tokens and sessions from being decrypted. Filesystem protection for the key volume is required; it is separate from MongoDB and must not be publicly served or committed.
A multi-hotel production launch using Gmail restricted scopes requires planning for Google's verification/security-assessment requirements. This implementation does not bypass those requirements. See Google restricted-scope verification.
6. Acceptance and rollback
Verify separate hotels cannot read or edit each other's records; save and reload settings; restart services and confirm persistence; import test messages twice without duplicates; check the worker resumes a paginated import; confirm no mail is sent without explicit staff approval and that default-disabled sending remains blocked. Review the activity log and Google account used by the connection.
Keep reviewed image IDs and release images for rollback and backups of both MongoDB and the key volume. Do not remove named volumes to fix application errors. The initial release has no automatic schema migration that destroys data. Use the operational preflight, encrypted backup and isolated restore drill, and establish retention and off-server copies before importing real guest data. /health/ready checks database reachability; the owner's Workspace health page also reports worker heartbeat and mailbox/reply exceptions.
Before accepting the host, run the confirmation-gated persistence drill during an announced maintenance window:
python3 deploy/ops.py preflight
python3 deploy/ops.py persistence-drill --confirm-restart
python3 deploy/ops.py preflight
The drill restarts MongoDB, API and worker, then force-recreates the stateless application containers using the already selected images. It verifies that the resolved database and key volumes keep the same identities, MongoDB collection counts and indexes remain unchanged, a value protected before restart can still be decrypted, and loopback readiness recovers. It does not alter provider feature flags, upgrade images, validate public TLS, or replace the separate backup/restore drill.
Record the host, operator, start/end time, release record checksum, resolved image IDs, preflight output and drill result in the deployment acceptance record. Also verify Docker starts at boot and perform a controlled Debian reboot before Gate A approval. After reboot, run the online preflight and inspect the Workspace health page; do not infer worker health solely from API readiness.
Keep the debian-host and persistence records in the restricted evidence store. Start from deploy/debian-host-acceptance.example.json and deploy/persistence-acceptance.example.json; the examples deliberately fail until every supervised scenario has passed. Bind both records to the expected release identifiers and validate them together:
python3 deploy/debian_acceptance.py \
/secure/acceptance/debian-host.json \
/secure/acceptance/persistence.json \
--expected-commit FULL_40_CHARACTER_SHA \
--expected-release-record-sha256 RELEASE_RECORD_SHA256
The validator requires matching archive and image identities, separate operator and reviewer names, the approved host capacity, only ports 80 and 443 recorded as publicly reachable, disabled unaccepted external writes, exact passed scenario sets and no unresolved critical findings. It validates record structure, not the restricted evidence itself. Retain the records, validator output and their checksums outside Git.
The supplied Docker json-file logs are size-capped to protect the small pilot disk, but container recreation removes that container's local log history. Before host acceptance, route GuestOps and Nginx logs to the site's durable restricted logging system, or use a reviewed Docker logging override backed by persistent systemd journal storage. Prove that operators can retrieve pre-recreation logs without exposing request credentials or OAuth callback query strings. Central retention and alerting are completed under milestone 11.