Complete Ubuntu server setup report

This report covers all work performed on the Ubuntu VM during this conversation on 1 October 2026. It records the installation and checks made at that time; the server was not re-inspected when this report was written. Passwords, tunnel tokens, and private keys are intentionally omitted.

1. Server and scope

ItemValue observed
Hostnameobsidiancrdt
SSH accountyuri
Tailscale IP100.113.222.40
LAN IP192.168.1.23
Tailscale DNS nameobsidiancrdt.taild38aa7.ts.net
Operating systemUbuntu 26.04.1 LTS
Kernel7.0.0-34-generic
Architecturex86_64 / amd64
RAMApproximately 3.3 GiB
SwapApproximately 3.8 GiB
Root filesystemApproximately 98 GiB total, 86 GiB available before installation

The VM received an independent Team Relay installation. No accounts, shares, notes, or configuration were migrated from the Raspberry Pi. The Pi was not changed during this work.

2. Access and initial inspection

  • Connected by SSH to yuri@100.113.222.40 using the supplied password.
  • Stored the VM’s SSH host key in the local workstation’s work/ssh_known_hosts file.
  • Used sudo with the supplied password for administrative work.
  • Disabled shell command history for the installation sessions and terminal input echo before sending configuration commands containing secrets.
  • Inspected memory, disk space, listening ports, Docker availability, and Tailscale state.
  • Found that Docker was not installed.
  • Found that Tailscale was installed and connected, with no existing Serve configuration and no HTTPS certificate domains enabled for this tailnet.
  • Closed the SSH sessions after completing the work.

No SSH settings, login passwords, firewall rules, router settings, or operating-system release were changed. No reboot was performed.

3. Installed packages

Refreshed Ubuntu’s APT package indexes and installed these packages and their required dependencies:

docker.io
docker-compose-v2
git
curl
openssl

Enabled and started the docker systemd service. Docker was subsequently confirmed enabled at boot.

Later, installed cloudflared through Cloudflare’s official APT repository, as described in section 9.

4. Team Relay release and source

  • Queried the project’s GitHub latest-release API.
  • It returned v1.13.4, published on 24 September 2026.
  • Cloned the repository at that release tag into:
/opt/evc-team-relay
  • Used a shallow clone of tag v1.13.4.
  • Ran the upstream published-image download script with version 1.13.4.
  • Downloaded the published amd64 images for control-plane, relay-server, and web-publish.
  • The upstream script also created local infra-…:latest image tags. The actual deployment references the explicit 1.13.4 release tags.
  • Download and startup commands completed successfully.

Repository: https://github.com/entire-vc/evc-team-relay

Release: https://github.com/entire-vc/evc-team-relay/releases/tag/v1.13.4

5. Docker deployment

Created a custom deployment directory:

/opt/evc-team-relay/deployment

The directory was created with permissions 700. The Docker Compose project is named evc-team-relay.

Persistent containers

ServiceImage / releasePurpose
postgrespostgres:16User, account, share, and application database
miniopgsty/minio, using the exact release and digest from the upstream v1.13.4 Compose filePrivate S3-compatible storage
control-planeghcr.io/entire-vc/evc-team-relay/control-plane:1.13.4API, authentication, administration, and share management
relay-serverghcr.io/entire-vc/evc-team-relay/relay-server:1.13.4Collaborative document synchronization
web-publishghcr.io/entire-vc/evc-team-relay/web-publish:1.13.4Published-note website
caddycaddy:2Local HTTP reverse proxy

All six persistent services use the restart policy unless-stopped.

One-time initialization containers

ServicePurpose
minio-initUses the upstream pinned pgsty/mc image to create bucket relay and disable anonymous bucket access
control-plane-migrateRuns python -m alembic upgrade head to initialize/migrate PostgreSQL

The initialization services use restart policy no. Compose dependencies wait for the database/storage to become healthy and for the initialization jobs to finish before starting dependent application services.

Health checks

Configured database readiness with pg_isready, MinIO readiness through its HTTP health endpoint, control-plane readiness through /health/ready, and relay readiness through /ready. Publishing’s image also reports health status.

Persistent data

Host directoryContainer location
deployment/data/postgres/var/lib/postgresql/data
deployment/data/minio/data
deployment/data/relay/app/data
deployment/data/uploads/app/app/static/uploads

The database and object-storage services have no published host ports. They communicate with the application services through the Compose network.

6. Configuration and generated credentials

Created these files:

FileContents
/opt/evc-team-relay/deployment/compose.jsonDocker Compose service definitions
/opt/evc-team-relay/deployment/.envApplication settings and private credentials
/opt/evc-team-relay/deployment/relay.tomlRelay server URL, auth public key, and storage settings
/opt/evc-team-relay/deployment/CaddyfileHTTP proxy routes

The .env, relay.toml, and compose.json files were set to permissions 600. The Compose file also contains private database/storage environment values, so it must be treated as a secrets file.

Generated on the VM:

  • A random administrator password.
  • A random PostgreSQL password.
  • A random MinIO password.
  • A random JWT secret.
  • An Ed25519 relay signing-key pair using OpenSSL.

The private signing key is base64-encoded in .env. Its corresponding raw public key is configured in relay.toml.

Application identity

Administrator login: admin@example.com
Server name: Ubuntu VM Team Relay
Server ID: obsidiancrdt-team-relay
Relay key ID: obsidiancrdt_relay_01
Database user/name: relay
Storage bucket: relay

admin@example.com is the initial application login identifier. Email sending is disabled.

Private handoff file

Saved the administrator credentials and connection addresses to:

/home/yuri/evc-team-relay-handoff/credentials.md

The directory has permissions 700; the credentials file has permissions 600. Both are owned by yuri.

Read your password in your own terminal:

ssh yuri@100.113.222.40
cat ~/evc-team-relay-handoff/credentials.md

No credentials were included in the downloadable reports.

Features disabled or not configured

  • Email sending: EMAIL_ENABLED=false.
  • CRM integration: ARGUS_ENABLED=false.
  • Lifecycle email features: LIFECYCLE_ENABLED=false.
  • SMTP, OAuth, monitoring dashboards, marketing workers, and external integrations were not configured.

7. Caddy proxy and local ports

Caddy listens inside its container on ports 8000, 8001, and 8002. Docker publishes them only on the VM’s loopback address:

VM addressContainer portDestination
127.0.0.1:180008000control-plane:8000
127.0.0.1:180018001relay-server:8080
127.0.0.1:180028002web-publish:3000

Caddy’s automatic HTTPS is disabled because the intended external HTTPS endpoint is Cloudflare.

Configured these proxy behaviors:

  • Block control-plane /metrics* with HTTP 404.
  • Permit request bodies up to 25 MB on the control-plane route.
  • Add X-Content-Type-Options: nosniff and X-Frame-Options: DENY to the control-plane route.
  • Preserve WebSocket paths and authentication query parameters for relay WebSocket connections.
  • For other relay requests carrying token in the query, move it to an Authorization: Bearer … header and remove it from the query before forwarding.
  • Forward the publishing service through the third local port.

Because ports 18000–18002 bind only to localhost, another computer cannot reach them directly. The installed cloudflared service runs on this same VM and can reach them.

8. Initial Tailscale configuration attempt

Initially prepared these private addresses:

https://obsidiancrdt.taild38aa7.ts.net
wss://obsidiancrdt.taild38aa7.ts.net:8443
https://obsidiancrdt.taild38aa7.ts.net:9443

Attempted to enable Tailscale Serve for HTTPS port 443. Tailscale reported that Serve was not enabled on the tailnet and returned an account-side activation link.

You chose to use Cloudflare instead. The waiting Serve command was cancelled. Tailscale subsequently reported No serve config. No Tailscale HTTPS proxy was left running, and no Tailscale account settings were changed.

The private addresses were later replaced in Team Relay’s configuration by the Cloudflare addresses below.

9. Cloudflare Tunnel installation

Installed the tunnel using the repository and token you supplied.

Repository setup

  • Ensured /usr/share/keyrings existed with mode 0755.
  • Downloaded Cloudflare’s public signing key from:
https://pkg.cloudflare.com/cloudflare-public-v2.gpg
  • Saved it at:
/usr/share/keyrings/cloudflare-public-v2.gpg
  • Created /etc/apt/sources.list.d/cloudflared.list with:
deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main
  • Refreshed APT indexes and installed cloudflared.

Service registration

  • Ran cloudflared service install with the supplied tunnel token.
  • The service installation completed successfully.
  • Installed version observed: cloudflared 2026.9.3, built 24 September 2026.
  • Confirmed systemd service cloudflared was active and enabled at boot.
  • Set /etc/systemd/system/cloudflared.service to mode 600, because the service definition contains the tunnel credential.
  • Checked the service journal for connection registrations: four tunnel connections were registered.

The tunnel token was used only for connecting this server to your existing tunnel. It does not provide Cloudflare DNS or hostname-route administration. No Cloudflare dashboard routes or DNS records were created during this conversation.

10. Public hostname configuration

After you provided domain rothirsch.net, selected:

HostnamePurposeCloudflare service target
team.rothirsch.netAPI, login, and administrator dashboardhttp://127.0.0.1:18000
relay.rothirsch.netCollaboration, including WebSocketshttp://127.0.0.1:18001
notes.rothirsch.netPublished notes, optionalhttp://127.0.0.1:18002

These routes should use HTTP as the service type in the Cloudflare tunnel configuration. External clients use HTTPS/WSS through Cloudflare.

Updated application settings

CONTROL_PLANE_PUBLIC_URL=https://team.rothirsch.net
RELAY_PUBLIC_URL=wss://relay.rothirsch.net
RELAY_AUDIENCE=https://relay.rothirsch.net
WEB_PUBLISH_DOMAIN=notes.rothirsch.net

Updated CORS_ALLOWED_ORIGINS to include:

https://team.rothirsch.net
https://notes.rothirsch.net
app://obsidian.md
capacitor://localhost
http://localhost

Updated the [server] URL in relay.toml to https://relay.rothirsch.net, matching the token audience exactly.

Updated the publishing container’s environment:

CONTROL_PLANE_URL=http://control-plane:8000
PUBLIC_CONTROL_PLANE_URL=https://team.rothirsch.net
PUBLIC_WEB_DOMAIN=notes.rothirsch.net
ORIGIN=https://notes.rothirsch.net

Updated the addresses in the private credentials handoff file. Passwords and signing keys remained unchanged.

Applied the deployment with Compose and explicitly restarted relay-server so it loaded the changed TOML configuration. Relay readiness returned HTTP 200 after that restart.

Addresses to use after creating the Cloudflare routes

Obsidian plugin server: https://team.rothirsch.net
Admin dashboard: https://team.rothirsch.net/admin-ui/login
Relay endpoint: wss://relay.rothirsch.net
Publishing: https://notes.rothirsch.net

Public routing remained a user step at the end of the installation. External HTTPS and actual Obsidian operation through Cloudflare were not verified.

11. Backups

Installed a root-only executable helper:

/usr/local/sbin/evc-team-relay-backup

Its behavior:

  1. Uses strict shell error handling and private file creation permissions (umask 077).
  2. Creates /var/backups/evc-team-relay if needed.
  3. Stops the Docker stack for a consistent archive.
  4. Archives the entire deployment directory, including configuration, secrets, and data.
  5. Uses an exit trap to start the stack again.

Created this initial backup:

/var/backups/evc-team-relay/evc-team-relay-20261001T075912Z.tar.gz

That backup predates the Cloudflare hostname changes. A restore from it would restore the original Tailscale address settings. Create a new backup to include the final Cloudflare configuration:

sudo /usr/local/sbin/evc-team-relay-backup

No automatic backup schedule, retention policy, or off-device backup was configured. Backups include credentials and must be kept private. Local backups do not protect against loss of the VM’s disk.

12. Checks performed and results

CheckResult at the time
Package installationSuccessful
Published Team Relay image downloadSuccessful
Initial Compose startupSuccessful
Database migration / bucket initializationCompleted
Persistent containersAll six running
PostgreSQL, MinIO, control plane, relay, publishingReported healthy
Administrator API authenticationSuccessful; access token not printed
Local API /health/readyHTTP 200
Local relay /readyHTTP 200
Local publishing rootHTTP 200
Local administrator login pageHTTP 200
Control-plane metrics through CaddyHTTP 404
Stack restart after initial backupSuccessful
Docker enabled at bootConfirmed
Host ports 18000–18002Confirmed bound to 127.0.0.1 only
Tailscale Serve configurationNone after cancellation
Cloudflare serviceActive and enabled
Cloudflare tunnel registrationFour connections registered
Public URL settings inside control-plane containerConfirmed new API, relay, and audience values
Relay after explicit configuration restartHTTP 200

During the cold backup, local HTTP checks briefly returned no response because the stack was deliberately stopped. Checks were repeated after startup and returned the successful results above.

No full automated test suite was run. Simultaneous Obsidian editing, attachment transfer, public Cloudflare connectivity, and restoration from backup were not exercised.

13. Logs retained on the VM

Installation command output was redirected to temporary log files:

/tmp/team-relay-prerequisites.log
/tmp/team-relay-clone.log
/tmp/team-relay-images.log
/tmp/team-relay-start.log
/tmp/cloudflared-install.log
/tmp/team-relay-domain-update.log
/tmp/team-relay-relay-restart.log

Files under /tmp are temporary and may disappear after system cleanup or a reboot. Runtime logs are available through Docker and systemd.

14. Maintenance commands

Run these on the Ubuntu VM:

# Docker service status
cd /opt/evc-team-relay/deployment
sudo docker compose -f compose.json ps
 
# Recent application logs
sudo docker compose -f compose.json logs --tail=100
 
# Start/apply the deployment
sudo docker compose -f compose.json up -d
 
# Stop the deployment
sudo docker compose -f compose.json stop
 
# Consistent backup; briefly interrupts service
sudo /usr/local/sbin/evc-team-relay-backup
 
# Cloudflare service state and recent logs
sudo systemctl is-active cloudflared
sudo systemctl is-enabled cloudflared
sudo journalctl -u cloudflared --since '-10 minutes' --no-pager

Review runtime output before sharing it; service definitions and configuration files contain credentials. Team Relay’s application images use explicit release tags, while postgres:16 and caddy:2 are moving major-version tags. Back up before future upgrades.

15. Work not performed

  • No data migration from the Pi.
  • No Obsidian client configuration on the VM.
  • No Cloudflare public hostname routes or DNS records created.
  • No public connectivity or co-editing checks through Cloudflare.
  • No SMTP or email invitation delivery configured.
  • No automated or off-device backups configured.
  • No Team Relay MCP server installed.
  • No ChatGPT integration or Agent Key created. The later API/MCP discussion was explanatory only.

16. References