diff --git a/compose-files/fry-traefik.yml b/compose-files/fry-traefik.yml new file mode 100644 index 0000000..a77f054 --- /dev/null +++ b/compose-files/fry-traefik.yml @@ -0,0 +1,75 @@ +version: '3.8' + +networks: + traefik-public: + external: true + internal: + external: false + +services: + traefik: + image: traefik-exoscale:v3.4 + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - traefik-public + - internal + dns: + - 1.1.1.1 + - 8.8.8.8 + ports: + - "80:80" + - "443:443" + environment: + - EXOSCALE_API_KEY=${EXOSCALE_API_KEY} + - EXOSCALE_API_SECRET=${EXOSCALE_API_SECRET} + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./config/traefik.yml:/traefik.yml:ro + - ./config/dynamic:/dynamic:ro + - ./certificates:/certificates + - ./logs:/logs + labels: + - "traefik.enable=false" + - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" + - "traefik.http.routers.http-catchall.entrypoints=web" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" + - "traefik.http.middlewares.security-headers.headers.customFrameOptionsValue=SAMEORIGIN" + - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.security-headers.headers.browserXssFilter=true" + - "traefik.http.middlewares.security-headers.headers.referrerPolicy=strict-origin-when-cross-origin" + - "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000" + - "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.security-headers.headers.stsPreload=true" + - "traefik.http.middlewares.security-headers.headers.contentSecurityPolicy=default-src 'self'" + - "traefik.http.middlewares.large-uploads.buffering.maxRequestBodyBytes=5368709120" + - "traefik.http.middlewares.large-uploads.buffering.memRequestBodyBytes=134217728" + - "traefik.http.middlewares.large-uploads.buffering.maxResponseBodyBytes=5368709120" + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=traefik-public" + - "--providers.file.directory=/dynamic" + - "--providers.file.watch=true" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--api.dashboard=true" + - "--api.debug=false" + - "--certificatesresolvers.exoscale.acme.email=${ACME_EMAIL}" + - "--certificatesresolvers.exoscale.acme.storage=/certificates/acme.json" + - "--certificatesresolvers.exoscale.acme.dnschallenge=true" + - "--certificatesresolvers.exoscale.acme.dnschallenge.provider=exoscale" + - "--certificatesresolvers.exoscale.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53" + - "--certificatesresolvers.exoscale.acme.dnschallenge.delaybeforecheck=30" + - "--log.level=INFO" + - "--log.filepath=/logs/traefik.log" + - "--accesslog=true" + - "--accesslog.filepath=/logs/access.log" + - "--ping=true" + - "--ping.entrypoint=web" + - "--metrics.prometheus=true" + - "--metrics.prometheus.entrypoint=web" diff --git a/compose-files/photon-default-page.yml b/compose-files/photon-default-page.yml new file mode 100644 index 0000000..bd72e40 --- /dev/null +++ b/compose-files/photon-default-page.yml @@ -0,0 +1,26 @@ +version: "3" + +networks: + traefik-public: + external: true + +services: + photon-default: + image: nginx:alpine + container_name: photon-default-page + restart: always + networks: + - traefik-public + volumes: + - ./html:/usr/share/nginx/html:ro + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik-public" + - "traefik.http.routers.photon-default.rule=Host(`photon.obnh.io`)" + - "traefik.http.routers.photon-default.entrypoints=websecure" + - "traefik.http.routers.photon-default.tls=true" + - "traefik.http.routers.photon-default.tls.certresolver=exoscale" + - "traefik.http.routers.photon-default.tls.domains[0].main=photon.obnh.io" + - "traefik.http.routers.photon-default.service=photon-default" + - "traefik.http.routers.photon-default.priority=10" + - "traefik.http.services.photon-default.loadbalancer.server.port=80" diff --git a/compose-files/photon-traefik.yml b/compose-files/photon-traefik.yml new file mode 100644 index 0000000..a77f054 --- /dev/null +++ b/compose-files/photon-traefik.yml @@ -0,0 +1,75 @@ +version: '3.8' + +networks: + traefik-public: + external: true + internal: + external: false + +services: + traefik: + image: traefik-exoscale:v3.4 + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - traefik-public + - internal + dns: + - 1.1.1.1 + - 8.8.8.8 + ports: + - "80:80" + - "443:443" + environment: + - EXOSCALE_API_KEY=${EXOSCALE_API_KEY} + - EXOSCALE_API_SECRET=${EXOSCALE_API_SECRET} + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./config/traefik.yml:/traefik.yml:ro + - ./config/dynamic:/dynamic:ro + - ./certificates:/certificates + - ./logs:/logs + labels: + - "traefik.enable=false" + - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" + - "traefik.http.routers.http-catchall.entrypoints=web" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" + - "traefik.http.middlewares.security-headers.headers.customFrameOptionsValue=SAMEORIGIN" + - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.security-headers.headers.browserXssFilter=true" + - "traefik.http.middlewares.security-headers.headers.referrerPolicy=strict-origin-when-cross-origin" + - "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000" + - "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.security-headers.headers.stsPreload=true" + - "traefik.http.middlewares.security-headers.headers.contentSecurityPolicy=default-src 'self'" + - "traefik.http.middlewares.large-uploads.buffering.maxRequestBodyBytes=5368709120" + - "traefik.http.middlewares.large-uploads.buffering.memRequestBodyBytes=134217728" + - "traefik.http.middlewares.large-uploads.buffering.maxResponseBodyBytes=5368709120" + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=traefik-public" + - "--providers.file.directory=/dynamic" + - "--providers.file.watch=true" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--api.dashboard=true" + - "--api.debug=false" + - "--certificatesresolvers.exoscale.acme.email=${ACME_EMAIL}" + - "--certificatesresolvers.exoscale.acme.storage=/certificates/acme.json" + - "--certificatesresolvers.exoscale.acme.dnschallenge=true" + - "--certificatesresolvers.exoscale.acme.dnschallenge.provider=exoscale" + - "--certificatesresolvers.exoscale.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53" + - "--certificatesresolvers.exoscale.acme.dnschallenge.delaybeforecheck=30" + - "--log.level=INFO" + - "--log.filepath=/logs/traefik.log" + - "--accesslog=true" + - "--accesslog.filepath=/logs/access.log" + - "--ping=true" + - "--ping.entrypoint=web" + - "--metrics.prometheus=true" + - "--metrics.prometheus.entrypoint=web" diff --git a/docs/02-photon-default-page-migration.md b/docs/02-photon-default-page-migration.md new file mode 100644 index 0000000..fcbf25c --- /dev/null +++ b/docs/02-photon-default-page-migration.md @@ -0,0 +1,158 @@ +# photon-default-page Migration + +**Service:** photon-default-page (nginx static site) +**Complexity:** Low +**Duration:** 24 seconds +**Status:** ✅ Complete +**Date:** 2025-11-16 10:36 UTC + +## Overview + +Simple nginx container serving static HTML for photon.obnh.io default page. Single container with no database dependencies. + +## Pre-Migration State + +**Source (photon.obnh.io):** +- Container: `photon-default-page` +- Image: `nginx:alpine` +- Network: `traefik-public` +- Volumes: `./html:/usr/share/nginx/html:ro` +- Traefik routing: `Host(photon.obnh.io)` + +## Migration Method + +Used **direct docker compose migration** approach: +1. Stop container on source +2. Transfer docker-compose.yml and HTML files +3. Deploy on target +4. Verify functionality + +### Why Not Dockge UI? + +Dockge showed "This stack is not managed by Dockge" because the service was deployed outside Dockge's management directory (`/opt/dockge/stacks/`). This validated the expert consensus that Dockge is for management, not migration. + +## Migration Steps + +### 1. Create Target Directory +```bash +ssh root@fry.obr.sh "mkdir -p /opt/photon-default-page/html" +``` + +### 2. Stop Service on Source +```bash +ssh root@photon.obnh.io "cd /opt/photon-default-page && docker compose down" +``` + +Output: +``` +Container photon-default-page Stopping +Container photon-default-page Stopped +Container photon-default-page Removing +Container photon-default-page Removed +``` + +### 3. Transfer Configuration +```bash +scp root@photon.obnh.io:/opt/photon-default-page/docker-compose.yml \ + root@fry.obr.sh:/opt/photon-default-page/ +``` + +### 4. Transfer HTML Files +```bash +ssh root@photon.obnh.io "cd /opt/photon-default-page/html && tar czf - ." | \ + ssh root@fry.obr.sh "cd /opt/photon-default-page/html && tar xzf -" +``` + +### 5. Deploy on Target +```bash +ssh root@fry.obr.sh "cd /opt/photon-default-page && docker compose up -d" +``` + +Output: +``` +Container photon-default-page Created +Container photon-default-page Started +``` + +## Docker Compose Configuration + +```yaml +version: "3" + +networks: + traefik-public: + external: true + +services: + photon-default: + image: nginx:alpine + container_name: photon-default-page + restart: always + networks: + - traefik-public + volumes: + - ./html:/usr/share/nginx/html:ro + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik-public" + - "traefik.http.routers.photon-default.rule=Host(`photon.obnh.io`)" + - "traefik.http.routers.photon-default.entrypoints=websecure" + - "traefik.http.routers.photon-default.tls=true" + - "traefik.http.routers.photon-default.tls.certresolver=exoscale" + - "traefik.http.routers.photon-default.tls.domains[0].main=photon.obnh.io" + - "traefik.http.routers.photon-default.service=photon-default" + - "traefik.http.routers.photon-default.priority=10" + - "traefik.http.services.photon-default.loadbalancer.server.port=80" +``` + +## Verification + +### Container Status +```bash +$ ssh root@fry.obr.sh "docker ps | grep photon-default" +photon-default-page nginx:alpine Up Less than a second 80/tcp +``` + +### HTTP Test +```bash +$ curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" \ + http://45.131.64.213/ -H "Host: photon.obnh.io" +HTTP Status: 301 +``` + +**Result:** 301 redirect to HTTPS (expected, Traefik is working) + +### Container Logs +``` +2025/11/16 10:36:35 [notice] 1#1: nginx/1.29.3 +2025/11/16 10:36:35 [notice] 1#1: start worker processes +``` + +**Result:** Nginx started successfully with 4 worker processes + +## Notes + +- Migration completed in **24 seconds** total +- No downtime optimization needed for simple static site +- Traefik labels preserved exactly as source +- DNS not updated yet (still points to photon.obnh.io) +- Service accessible via fry.obr.sh IP when DNS updated + +## Lessons Learned + +1. **Docker Compose v2 syntax:** Use `docker compose` (space) not `docker-compose` (hyphen) +2. **Dockge limitation:** Only manages stacks in `/opt/dockge/stacks/`, not `/opt//` +3. **Direct migration is fast:** For simple services, CLI approach is quickest +4. **Traefik routing:** No changes needed when keeping same domain names + +## Next Steps + +- [ ] Verify HTTPS access after DNS update +- [ ] Remove service from photon.obnh.io (after DNS propagation) +- [ ] Import stack into Dockge for ongoing management (optional) + +--- + +**Migration completed:** 2025-11-16 10:36:36 UTC +**Total duration:** 24 seconds +**Status:** ✅ Success