# Production migration and rollback This runbook moves the titan deployment from the Python source tree at `/root/repo/urlshortener` to the published `sodium/ushort` image. The final server-side application directory is `/root/compose/ushort`; no source checkout is required on titan. Do not cut over until the exact image tag has been built for both `linux/amd64` and `linux/arm64`, pushed, and recorded by digest. ## 1. Prepare without affecting production Create this layout on titan: ```text /root/compose/ushort/ ├── docker-compose.yml ├── config.toml └── data/ ``` Copy `deploy/compose.production.yml` to the server as `docker-compose.yml`. Convert the live JSON settings to TOML using `config.example.toml` as the reference, with these cutover-specific rules: - keep the live `base_url`, limits, retention, and rate-limit values; - set `db_path = "data/urlshort.db"`; - set `production = false` so the binary serves its embedded frontend; and - generate a new API key, because the old key existed in the legacy Git history. Keep `config.toml` owned by `1001:1001` with mode `0400`, matching the container identity. Do not copy it into Git or a container image. Validate the deployment file before stopping anything: ```bash cd /root/compose/ushort chown 1001:1001 config.toml chmod 0400 config.toml docker-compose config docker pull sodium/ushort:0.1.1 ``` ## 2. Take a consistent database copy The final copy must be made while the Python service is stopped so no committed row is missed and no SQLite journal is in flight: ```bash cd /root/repo/urlshortener docker-compose stop urlshort cp -a data/urlshort.db /root/compose/ushort/data/urlshort.db cp -a data/urlshort.db /root/compose/ushort/data/urlshort.db.pre-rust chown -R 1001:1001 /root/compose/ushort/data chmod 0750 /root/compose/ushort/data chmod 0640 /root/compose/ushort/data/urlshort.db* ``` Do not delete or alter the original database during the initial soak period. ## 3. Start and validate ushort locally ```bash cd /root/compose/ushort docker-compose up -d docker-compose ps docker-compose logs --tail=100 ushort curl -i http://127.0.0.1:18082/s/api/health curl -I http://127.0.0.1:18082/s/ ``` Expected results are HTTP 200 health JSON and HTTP 200 HTML. Confirm the container does not restart and the database remains writable. ## 4. Switch nginx Replace the old `include /root/repo/urlshortener/nginx.conf;` in the `xcel.me` vhost with the location blocks from the new `nginx.conf`. Those blocks proxy the frontend as well as API/redirect traffic to `127.0.0.1:18082`; they do not refer to a source or static-file directory. Test before reload: ```bash docker exec nginx nginx -t docker exec nginx nginx -s reload ``` Then validate through the public endpoint: ```bash curl -i https://xcel.me/s/api/health curl -I https://xcel.me/s/ curl -I https://xcel.me/s/static/app.js ``` Check that `/s/` is `no-store`, static assets are `no-cache`, an existing short code still returns the original 302 target, and its visit count increments once. ## 5. Soak and clean up During the soak period, monitor: ```bash cd /root/compose/ushort docker-compose ps docker-compose logs --tail=200 ushort ``` After the rollback window closes: 1. retain a protected database backup; 2. remove the obsolete `/root/repo/urlshortener` source tree; 3. remove its `/srv/urlshortener` bind mount from the global nginx Compose file; and 4. recreate nginx and re-run `nginx -t`. At that point, application source exists only in Gitea, while titan contains only the Compose file, TOML config, and data under `/root/compose/ushort`. ## Rollback If validation fails before public traffic is enabled: ```bash cd /root/compose/ushort docker-compose down cd /root/repo/urlshortener docker-compose start urlshort ``` Restore the old nginx include and reload nginx. If public writes occurred after cutover, stop ushort first and copy its current database back to the legacy data path before starting Python; otherwise those new short URLs would be lost. The schema is deliberately identical, so no reverse schema migration is required.