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
| Item | Value observed |
|---|---|
| Hostname | obsidiancrdt |
| SSH account | yuri |
| Tailscale IP | 100.113.222.40 |
| LAN IP | 192.168.1.23 |
| Tailscale DNS name | obsidiancrdt.taild38aa7.ts.net |
| Operating system | Ubuntu 26.04.1 LTS |
| Kernel | 7.0.0-34-generic |
| Architecture | x86_64 / amd64 |
| RAM | Approximately 3.3 GiB |
| Swap | Approximately 3.8 GiB |
| Root filesystem | Approximately 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.40using the supplied password. - Stored the VM’s SSH host key in the local workstation’s
work/ssh_known_hostsfile. - Used
sudowith 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
opensslEnabled 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, andweb-publish. - The upstream script also created local
infra-…:latestimage tags. The actual deployment references the explicit1.13.4release 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/deploymentThe directory was created with permissions 700. The Docker Compose project is named evc-team-relay.
Persistent containers
| Service | Image / release | Purpose |
|---|---|---|
postgres | postgres:16 | User, account, share, and application database |
minio | pgsty/minio, using the exact release and digest from the upstream v1.13.4 Compose file | Private S3-compatible storage |
control-plane | ghcr.io/entire-vc/evc-team-relay/control-plane:1.13.4 | API, authentication, administration, and share management |
relay-server | ghcr.io/entire-vc/evc-team-relay/relay-server:1.13.4 | Collaborative document synchronization |
web-publish | ghcr.io/entire-vc/evc-team-relay/web-publish:1.13.4 | Published-note website |
caddy | caddy:2 | Local HTTP reverse proxy |
All six persistent services use the restart policy unless-stopped.
One-time initialization containers
| Service | Purpose |
|---|---|
minio-init | Uses the upstream pinned pgsty/mc image to create bucket relay and disable anonymous bucket access |
control-plane-migrate | Runs 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 directory | Container 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:
| File | Contents |
|---|---|
/opt/evc-team-relay/deployment/compose.json | Docker Compose service definitions |
/opt/evc-team-relay/deployment/.env | Application settings and private credentials |
/opt/evc-team-relay/deployment/relay.toml | Relay server URL, auth public key, and storage settings |
/opt/evc-team-relay/deployment/Caddyfile | HTTP 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: relayadmin@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.mdThe 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.mdNo 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 address | Container port | Destination |
|---|---|---|
127.0.0.1:18000 | 8000 | control-plane:8000 |
127.0.0.1:18001 | 8001 | relay-server:8080 |
127.0.0.1:18002 | 8002 | web-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: nosniffandX-Frame-Options: DENYto the control-plane route. - Preserve WebSocket paths and authentication query parameters for relay WebSocket connections.
- For other relay requests carrying
tokenin the query, move it to anAuthorization: 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:9443Attempted 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/keyringsexisted with mode0755. - 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.listwith:
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 installwith the supplied tunnel token. - The service installation completed successfully.
- Installed version observed: cloudflared 2026.9.3, built 24 September 2026.
- Confirmed systemd service
cloudflaredwas active and enabled at boot. - Set
/etc/systemd/system/cloudflared.serviceto mode600, 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:
| Hostname | Purpose | Cloudflare service target |
|---|---|---|
team.rothirsch.net | API, login, and administrator dashboard | http://127.0.0.1:18000 |
relay.rothirsch.net | Collaboration, including WebSockets | http://127.0.0.1:18001 |
notes.rothirsch.net | Published notes, optional | http://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.netUpdated CORS_ALLOWED_ORIGINS to include:
https://team.rothirsch.net
https://notes.rothirsch.net
app://obsidian.md
capacitor://localhost
http://localhostUpdated 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.netUpdated 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.netPublic 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-backupIts behavior:
- Uses strict shell error handling and private file creation permissions (
umask 077). - Creates
/var/backups/evc-team-relayif needed. - Stops the Docker stack for a consistent archive.
- Archives the entire
deploymentdirectory, including configuration, secrets, and data. - Uses an exit trap to start the stack again.
Created this initial backup:
/var/backups/evc-team-relay/evc-team-relay-20261001T075912Z.tar.gzThat 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-backupNo 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
| Check | Result at the time |
|---|---|
| Package installation | Successful |
| Published Team Relay image download | Successful |
| Initial Compose startup | Successful |
| Database migration / bucket initialization | Completed |
| Persistent containers | All six running |
| PostgreSQL, MinIO, control plane, relay, publishing | Reported healthy |
| Administrator API authentication | Successful; access token not printed |
Local API /health/ready | HTTP 200 |
Local relay /ready | HTTP 200 |
| Local publishing root | HTTP 200 |
| Local administrator login page | HTTP 200 |
| Control-plane metrics through Caddy | HTTP 404 |
| Stack restart after initial backup | Successful |
| Docker enabled at boot | Confirmed |
| Host ports 18000–18002 | Confirmed bound to 127.0.0.1 only |
| Tailscale Serve configuration | None after cancellation |
| Cloudflare service | Active and enabled |
| Cloudflare tunnel registration | Four connections registered |
| Public URL settings inside control-plane container | Confirmed new API, relay, and audience values |
| Relay after explicit configuration restart | HTTP 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.logFiles 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-pagerReview 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.