# Deployment The Gitea Actions workflow in `.gitea/workflows/ci-cd.yml` verifies every pull request and every push to `main`. A successful push to `main` is then deployed over SSH. The server builds the exact Git archive tested by CI, starts the Compose service, and waits for its container health check. Releases are stored in `/releases/`. The `current` symlink is changed only after the new container is healthy. If startup fails, the deployment script automatically rebuilds and starts the previous release. ## Gitea repository secrets Add these secrets under the repository's **Settings > Actions > Secrets**: | Secret | Value | | --- | --- | | `DEPLOY_HOST` | Server hostname or IP address | | `DEPLOY_USER` | SSH user allowed to run Docker | | `DEPLOY_PORT` | SSH port; optional, defaults to `22` | | `DEPLOY_PATH` | Absolute server directory, for example `/srv/portfolio` | | `DEPLOY_SSH_KEY` | Private SSH key for the deployment user | | `DEPLOY_KNOWN_HOSTS` | Pinned SSH host-key line for the server | Generate the known-hosts value from a trusted machine and verify its fingerprint before saving it: ```sh ssh-keyscan -p 22 your-server.example.com ``` Do not copy an unverified key directly into the secret; doing so would remove the protection against a man-in-the-middle attack. ## One-time server setup The server needs Docker Engine with the Compose v2 plugin. The deployment user must be able to run `docker` without an interactive password prompt. Create the deployment root and the external network once: ```sh sudo mkdir -p /srv/portfolio/releases sudo chown -R deploy:deploy /srv/portfolio docker network create caddy_proxy ``` Replace `deploy:deploy` and `/srv/portfolio` with the values used for `DEPLOY_USER` and `DEPLOY_PATH`. Add the public half of `DEPLOY_SSH_KEY` to the deployment user's `~/.ssh/authorized_keys`. The standalone Next.js container joins `caddy_proxy` under the `portfolio-app` service name. Caddy must also join that network and proxy to `portfolio-app:3000`. No host port is published by this project. A minimal Caddy route is: ```caddyfile portfolio.example.com { reverse_proxy portfolio-app:3000 } ``` Caddy forwards the browser's `Accept` header by default; keep that behavior so Next.js can negotiate WebP responses. Do not replace the cache headers emitted for `/_next/image`. The named `next-image-cache` volume persists `.next/cache` between container replacements so optimized variants can be reused. The application expects immutable media URLs. When an admin or future upload workflow replaces an image, write it to a UUID or content-versioned URL instead of overwriting the previous object at the same URL. Next.js has no per-image optimizer-cache invalidation API. ## Manual deployment For an emergency manual deployment from a checkout on the server: ```sh docker compose --project-name portfolio up --detach --build --remove-orphans ``` Old versioned release directories are deliberately retained for rollback. They can be removed manually after a newer release has been proven stable; keep the directory referenced by the `current` symlink. ## Runtime verification The production image contains `.next/standalone`, `.next/static`, and `public`, and runs the generated `server.js` with Node.js 22 on port 3000. CI smoke-tests the English and Persian routes, a built static asset, and `/_next/image` with WebP negotiation before deployment.