Files
my-protfolio/DEPLOYMENT.md
Amirhossein Khalili f34bea05cd
Some checks failed
Test and deploy / Verify production build (push) Has been cancelled
Test and deploy / Deploy production (push) Has been cancelled
perf: optimize portfolio runtime delivery
Replace the blocking asset loader with progressive rendering, defer expensive media and Three.js work, and reduce hydration and animation costs. Move deployment from a static Nginx export to the standalone Next.js runtime with optimized dynamic images.
2026-09-09 12:43:37 +03:30

3.4 KiB

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 <DEPLOY_PATH>/releases/<commit-sha>. 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:

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:

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:

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:

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.