Files
ushort/DEPLOYMENT.md
T

4.0 KiB

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:

/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:

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:

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

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:

docker exec nginx nginx -t
docker exec nginx nginx -s reload

Then validate through the public endpoint:

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:

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:

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.