writeonce/docs/07-ssl.md
2026-04-05 00:45:02 +02:00

6.8 KiB

SSL and Deployment

Problem

In 01-problem.md, the infrastructure overhead was identified — multiple repos, AWS dependencies, separate deployment pipelines. But one problem went unaddressed: the server-side infrastructure that sits in front of the application — nginx reverse proxy, SSL certificates, systemd service management, and deployment to the production host.

Currently this requires manual SSH, manual nginx config, manual certbot runs. For a single-binary platform, the deployment should be as simple as the architecture.

Target

Given:

  • SSH access to writeonce.de is configured
  • nginx exists at the default path /etc/nginx/
  • The writeonce binary listens on a local port (e.g., 127.0.0.1:3000)

The deployment pipeline should:

  1. Build the binary
  2. Copy it to the server
  3. Create/update the systemd service
  4. Restart the service
  5. Configure nginx as a reverse proxy
  6. Obtain and auto-renew SSL certificates via Let's Encrypt

Systemd Service

The writeonce binary runs as a systemd service for automatic restart, logging, and boot-start.

Service File

# /etc/systemd/system/writeonce.service
[Unit]
Description=writeonce content platform
After=network.target

[Service]
Type=simple
User=writeonce
Group=writeonce
WorkingDirectory=/opt/writeonce
ExecStart=/opt/writeonce/writeonce
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/writeonce/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Directory Layout on Server

/opt/writeonce/
  writeonce              # the binary
  content/               # article .json + .md files
  data/                  # derived .seg + .idx (rebuilt on start)
  templates/             # .htmlx templates
  static/                # CSS, images

Service Management

# Install / update
sudo systemctl daemon-reload
sudo systemctl enable writeonce
sudo systemctl restart writeonce

# Check status
sudo systemctl status writeonce
journalctl -u writeonce -f

Nginx Reverse Proxy

Nginx sits in front of the writeonce binary, handling SSL termination and proxying requests to 127.0.0.1:3000.

Nginx Config

# /etc/nginx/sites-available/writeonce.de
server {
    listen 80;
    server_name writeonce.de www.writeonce.de;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name writeonce.de www.writeonce.de;

    ssl_certificate /etc/letsencrypt/live/writeonce.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/writeonce.de/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Keep connections open for database subscriptions
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

    # Static assets — let nginx serve directly for better caching
    location /static/ {
        alias /opt/writeonce/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Enable Site

sudo ln -sf /etc/nginx/sites-available/writeonce.de /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

SSL with Let's Encrypt

Initial Certificate

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d writeonce.de -d www.writeonce.de

Certbot modifies the nginx config to add SSL directives and obtains the certificate.

Auto-Renewal

Certbot installs a systemd timer that runs twice daily:

# Check timer
systemctl list-timers | grep certbot

# Manual test
sudo certbot renew --dry-run

Certificates auto-renew before expiry. Nginx reloads automatically via certbot's deploy hook.

Deploy Hook for Nginx Reload

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx

Deployment Script

A single script that builds, copies, and restarts:

#!/bin/bash
# deploy.sh — run from the development machine
set -e

SERVER="writeonce.de"
REMOTE_DIR="/opt/writeonce"

echo "Building release binary..."
cargo build --release -p wo-rt --bin writeonce

echo "Copying binary to server..."
scp target/release/writeonce $SERVER:$REMOTE_DIR/writeonce.new

echo "Syncing content and templates..."
rsync -az --delete content/ $SERVER:$REMOTE_DIR/content/
rsync -az --delete templates/ $SERVER:$REMOTE_DIR/templates/
rsync -az --delete static/ $SERVER:$REMOTE_DIR/static/

echo "Swapping binary and restarting..."
ssh $SERVER "
    sudo mv $REMOTE_DIR/writeonce.new $REMOTE_DIR/writeonce
    sudo systemctl restart writeonce
"

echo "Deployed. Checking status..."
ssh $SERVER "sudo systemctl status writeonce --no-pager"

First-Time Setup

Run once on the server to create the user, directory, and service:

#!/bin/bash
# setup.sh — run on the server
set -e

# Create user
sudo useradd -r -s /bin/false writeonce

# Create directory
sudo mkdir -p /opt/writeonce/{content,data,templates,static}
sudo chown -R writeonce:writeonce /opt/writeonce

# Install service
sudo cp writeonce.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable writeonce

# Configure nginx
sudo cp writeonce.de.nginx /etc/nginx/sites-available/writeonce.de
sudo ln -sf /etc/nginx/sites-available/writeonce.de /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# SSL
sudo certbot --nginx -d writeonce.de -d www.writeonce.de

What This Replaces

Before After
Pulumi IaC managing Lambda + S3 deploy.sh with scp + rsync
AWS Lambda deployment pipeline systemctl restart writeonce
S3 bucket for content hosting rsync content/ to server
Docker Compose for API + DB Single binary, one systemd service
Multiple nginx configs for API + frontend One nginx config, one proxy_pass
Manual SSL setup certbot --nginx with auto-renewal

Connection Keepalive for Subscriptions

The nginx config sets proxy_read_timeout 86400s (24 hours) to keep persistent connections open for the database subscription model. When a browser visits an article page and the connection transitions to Subscribed state, nginx must not timeout and close the upstream connection.

If nginx is removed in the future (the binary handles TLS directly via rustls), this concern disappears — the binary owns the socket end-to-end.