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/docker-compose.yml as the Compose file. 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 = falseso 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.0
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:
- retain a protected database backup;
- remove the obsolete
/root/repo/urlshortenersource tree; - remove its
/srv/urlshortenerbind mount from the global nginx Compose file; and - 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.