Why this was more than an image update
My Gitea installation had been running version 1.18.0 for several years. Upgrading a stateful service is not equivalent to replacing a stateless web container: Gitea owns database migrations, configuration, repositories, keys, and user-generated data.
Before changing the image, I backed up MariaDB and the persistent Gitea directories. I also recorded the current Compose configuration and container versions. That created both a recovery path and a baseline for post-upgrade comparison.
The service was eventually moved to a pinned Gitea 1.26.2 image:
services:
server:
image: gitea/gitea:1.26.2
Pinning the version makes future changes intentional. A floating latest tag can turn an ordinary container recreation into an unplanned application upgrade.
Database startup was initially misleading
Gitea reported:
dial tcp ...:3306: connect: connection refused
MariaDB later logged that it was ready for connections, while this command returned Access denied:
docker exec docker-mariadb-1 \
mariadb-admin ping -h 127.0.0.1
Those messages described different stages:
connection refusedmeant nothing was accepting TCP connections yet.Access deniedmeant MariaDB was accepting connections but rejected an unauthenticated root login.
Docker DNS confirmed that the stable address was the Compose service name:
docker exec gitea getent hosts mariadb
Gitea should connect to mariadb:3306, not a container IP that may change after recreation:
environment:
GITEA__database__HOST: mariadb:3306
Correcting the external URL
After the upgrade, Gitea warned that it detected:
http://code.minhdb.io/
That did not match the public HTTPS site. The application receives HTTP from Nginx internally, but users access HTTPS externally. Both sides need to describe that relationship accurately.
The Gitea configuration became:
environment:
GITEA__server__DOMAIN: code.minhdb.io
GITEA__server__PROTOCOL: http
GITEA__server__ROOT_URL: https://code.minhdb.io/
PROTOCOL describes Gitea’s internal listener. ROOT_URL is the canonical address used in links, emails, webhooks, OAuth callbacks, and clone URLs.
The Nginx proxy passes the original request information:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
This lets Gitea know that the original client used HTTPS even though the final proxy hop uses HTTP.
Cleaning deprecated configuration
Startup logs also identified an obsolete logging option:
[log].ROUTER ... use [log].logger.router.MODE
The active configuration now contains:
[log]
logger.router.MODE = console
I verified the file from both the host and container, checked for environment overrides, restarted Gitea, and searched only the new startup logs:
docker compose logs --since 1m server | grep -i deprecat
No output confirmed that the warning belonged to the previous process and had been resolved.
Validation checklist
After the upgrade, I verified:
- MariaDB migrations completed.
- Gitea reached its web listener.
- Existing users and repositories remained available.
- HTTPS links used the correct host and scheme.
- SSH clone URLs used the intended external port.
- Registration remained disabled.
- Startup logs contained no unresolved migration or deprecation errors.
- Nginx configuration passed
nginx -t.
The main lesson was to treat application, database, and proxy configuration as one system. An upgrade can succeed at the container level while still producing incorrect public URLs or unstable database connections.