Production Linux server install
A hardened Linux install on a single host — TLS via Let's Encrypt, firewall, non-root user, and the operational hooks to put real traffic on.
4 min read
This walk takes the quickstart shape and hardens it for production traffic. The result is a single Linux host running Tale behind real TLS, with a firewall, a non-root operator user, and the operational defaults the team should hit before pointing users at the URL.
The walk targets a recent Ubuntu LTS or Debian; commands translate one-for-one to RHEL-family distros with dnf substituted for apt. Skip nothing — the order matters, and each step assumes the previous one landed cleanly.
Before you begin
You need:
- A VM or bare-metal host with at least 8 GB RAM, 4 vCPU, and 100 GB disk. Storage grows with attachments and knowledge.
- A DNS A record pointing at the host's public IP. Without DNS, Let's Encrypt cannot issue a cert.
- Ports 80, 443 reachable from the public internet for TLS issuance; SSH on whatever port your operator policy says.
- Sudo on the host.
Step 1 — Provision the box
Update and install the basics:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git ufwCreate a non-root operator user named tale:
sudo adduser tale
sudo usermod -aG sudo,docker taleSwitch to that user (sudo su - tale) for the rest of the walk. Operating Tale as root pulls a higher blast radius for no benefit; the rest of the steps assume the tale user.
Step 2 — Install Docker
curl -fsSL https://get.docker.com | sudo sh
sudo systemctl enable --now dockerVerify with docker run hello-world. If the user cannot run docker without sudo, log out and back in to pick up the docker group membership.
Step 3 — Configure firewall and reverse path
Allow only what Tale needs:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableIf you front Tale with an existing reverse proxy on the same host (rare on a single-host install), set TLS_MODE=external in .env and adjust the firewall accordingly. The Caddy container inside Tale terminates TLS by default.
Step 4 — Pull Tale
git clone https://github.com/tale-project/tale.git
cd tale
cp .env.example .envSet HOST, SITE_URL, and generate the four secrets as in the quickstart. The production diff versus quickstart lives in step 5 (TLS) and the operational hooks at the end of this walk.
Step 5 — TLS via Let's Encrypt
Open .env and set:
| Variable | Value |
|---|---|
TLS_MODE | letsencrypt |
TLS_EMAIL | An ops mailbox you read |
Caddy issues and renews the cert automatically using the DNS record from the prerequisites. The first boot waits for the cert; expect a one-minute delay on the first docker compose up -d while the ACME challenge runs.
Step 6 — First boot
docker compose up -d
docker compose psEvery service should be running or healthy. Walk through Step 4 — Create the first admin from the quickstart to land in the dashboard. Open SITE_URL over https:// — the browser should not warn about the cert.
Step 7 — Operational hooks
Before pointing users at the URL, three hooks make life easier later:
- Backups. Point your existing snapshot tooling at
db-dataand the object store volume — see Backups and restore. - Logs. Tale logs to stdout. If the host has journald,
journalctl -u dockercarries everything; otherwise pipe to your aggregator. - Metrics. Set
METRICS_BEARER_TOKENin.envand scrape/metricsfrom your Prometheus — see Observability config.
Port table
| Port | Direction | Purpose | Required |
|---|---|---|---|
| 22 | inbound | SSH | yes, restricted |
| 80 | inbound | HTTP, used for ACME and 301 to HTTPS | yes |
| 443 | inbound | HTTPS, primary traffic | yes |
| 53 | outbound | DNS | yes |
| 443 | outbound | model providers, image pulls | yes |
Troubleshooting
- Let's Encrypt issuance fails. DNS must resolve to this host's public IP from the public internet, and port 80 must be reachable from the public internet. Run
curl -I http://$HOSTfrom another machine; if it hits the Caddy challenge, the path works. - Containers cannot reach model providers. The host's outbound firewall might block; verify with
docker compose exec platform curl -I https://api.openai.com. - TLS cert renews fail later. Caddy renews 30 days before expiry; failures show in
docker compose logs proxy. The two common causes are an expiredTLS_EMAILmailbox and a DNS change that broke the record.
Where this gets used
You now have a production-shaped install on one host. Two follow-ups belong on the calendar — Backups and restore and Hardening. If your scale outgrows one host (the rule of thumb is roughly a hundred concurrent users on the recommended spec), the multi-host architecture lives at Container architecture.