Use native Gitea contexts, audit the full dependency tree, and document the runner and branch-protection requirements needed for reliable deployments.
3.9 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 Actions setup
Enable Repository Actions in the repository settings and register a trusted
Gitea Runner. The workflow uses the ubuntu-latest label; that runner image must
provide Bash, Git, tar, OpenSSH (ssh and scp), and standard GNU utilities.
Node.js 22 is installed by the workflow.
After the first successful verification run, protect main in Gitea and make
the Verify production build status check required. This prevents a pull
request from merging when linting, type-checking, tests, the production build,
or the dependency audit fails.
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.